Page MenuHomePhorge

No OneTemporary

Size
105 KB
Referenced Files
None
Subscribers
None
diff --git a/build.rs b/build.rs
index 3e253f8..09fabe7 100644
--- a/build.rs
+++ b/build.rs
@@ -1,165 +1,176 @@
use chrono::Datelike;
use flate2::read::GzDecoder;
use reqwest;
use tar::Archive;
use std::{
env,
fs::{self, File},
io::{self, copy},
path::{Path, PathBuf},
process::Command,
};
const NODE_VERSION: &str = "20.10.0";
fn extract_tar_gz(tar: &PathBuf, download_dir: &PathBuf) -> io::Result<()> {
let file = File::open(tar)?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
archive.unpack(download_dir)?;
Ok(fs::remove_file(tar)?)
}
fn download_file(url: String, destination: &PathBuf, download_dir: &PathBuf) {
if !download_dir.exists() {
fs::create_dir_all(download_dir).unwrap();
}
let mut response = reqwest::blocking::get(url).expect("Failed to send request");
let mut file = File::create(destination).expect("Failed to create file");
copy(&mut response, &mut file).expect("Failed to copy content");
}
fn download_node() -> PathBuf {
#[cfg(target_os = "linux")]
let target_os = "linux";
#[cfg(all(target_os = "macos"))]
let target_os = "darwin";
#[cfg(all(target_arch = "arm"))]
let target_arch = "armv7l";
#[cfg(all(target_arch = "x86_64"))]
let target_arch = "x64";
#[cfg(all(target_arch = "aarch64"))]
let target_arch = "arm64";
let download_url = format!("https://nodejs.org/dist/v{NODE_VERSION}/node-v{NODE_VERSION}-{target_os}-{target_arch}.tar.gz");
/* paths */
let download_dir = Path::new("target").join("downloads");
let node_extract_dir = download_dir.join(format!("node-v{NODE_VERSION}-{target_os}-{target_arch}"));
if node_extract_dir.is_dir() {
return node_extract_dir;
}
/* download node */
let node_archive = download_dir.join(format!("node-v{}-{}.tar.gz", NODE_VERSION, target_os));
download_file(download_url, &node_archive, &download_dir);
/* extract node */
if let Err(err) = extract_tar_gz(&node_archive, &download_dir) {
panic!("Failed to extract Node.js: {:?}", err)
}
println!("cargo:rustc-env=NODE_HOME={}", node_extract_dir.to_str().unwrap());
return node_extract_dir;
}
fn download_then_build(node_extract_dir: PathBuf) {
let base_dir = match fs::canonicalize(node_extract_dir) {
Ok(path) => path,
Err(err) => panic!("{err}"),
};
let bin = &base_dir.join("bin");
let node = &bin.join("node");
let project_dir = &Path::new("src").join("webui");
let npm = &base_dir.join("lib/node_modules/npm/index.js");
/* set path */
let mut paths = match env::var_os("PATH") {
Some(paths) => env::split_paths(&paths).collect::<Vec<PathBuf>>(),
None => vec![],
};
paths.push(bin.clone());
let path = match env::join_paths(paths) {
Ok(joined) => joined,
Err(err) => panic!("{err}"),
};
/* install deps */
Command::new(node)
.args([npm.to_str().unwrap(), "ci"])
.current_dir(project_dir)
.env("PATH", &path)
.status()
.expect("Failed to install dependencies");
/* build frontend */
Command::new(node)
.args(["node_modules/astro/astro.js", "build"])
.current_dir(project_dir)
.env("PATH", &path)
.status()
.expect("Failed to build frontend");
}
fn main() {
#[cfg(target_os = "windows")]
compile_error!("This project is not supported on Windows.");
#[cfg(target_arch = "x86")]
compile_error!("This project is not supported on 32 bit.");
/* version attributes */
let date = chrono::Utc::now();
let profile = env::var("PROFILE").unwrap();
let output = Command::new("git").args(&["rev-parse", "--short=10", "HEAD"]).output().unwrap();
let output_full = Command::new("git").args(&["rev-parse", "HEAD"]).output().unwrap();
println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap());
println!("cargo:rustc-env=GIT_HASH={}", String::from_utf8(output.stdout).unwrap());
println!("cargo:rustc-env=GIT_HASH_FULL={}", String::from_utf8(output_full.stdout).unwrap());
println!("cargo:rustc-env=BUILD_DATE={}-{}-{}", date.year(), date.month(), date.day());
/* profile matching */
match profile.as_str() {
"debug" => println!("cargo:rustc-env=PROFILE=debug"),
"release" => {
println!("cargo:rustc-env=PROFILE=release");
#[allow(unused_must_use)]
for name in vec!["assets", "dist"] {
fs::remove_dir_all(format!("src/webui/{name}"));
}
/* pre-build */
let path = download_node();
download_then_build(path);
/* move assets */
fs::create_dir_all("src/webui/assets/").expect("Failed to move assets");
fs::rename("src/webui/dist/static", "src/webui/assets/static").expect("Failed to move assets");
/* cc linking */
cxx_build::bridge("src/lib.rs")
.file("lib/bridge.cc")
.file("lib/process.cc")
.file("lib/fork.cc")
.include("lib/include")
.flag_if_supported("-std=c++17")
.compile("bridge");
}
_ => println!("cargo:rustc-env=PROFILE=none"),
}
- let watched = vec!["lib", "src/lib.rs", "lib/include", "src/webui/src", "src/webui/*.mjs", "src/webui/*.json", "src/webui/*.yaml"];
+ let watched = vec![
+ "lib",
+ "src/lib.rs",
+ "lib/include",
+ "src/webui/src",
+ "src/webui/links.ts",
+ "src/webui/package.json",
+ "src/webui/tsconfig.json",
+ "src/webui/astro.config.mjs",
+ "src/webui/tailwind.config.mjs",
+ ];
+
watched.iter().for_each(|file| println!("cargo:rerun-if-changed={file}"));
}
diff --git a/errors.todo b/errors.todo
new file mode 100644
index 0000000..cf43dc6
--- /dev/null
+++ b/errors.todo
@@ -0,0 +1,2 @@
+@fix pmc daemon restart errors
+ - it infinite restarts
\ No newline at end of file
diff --git a/src/cli.rs b/src/cli.rs
index 70c5197..a1f33bf 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,593 +1,594 @@
use colored::Colorize;
use global_placeholders::global;
use macros_rs::{crashln, string, ternary};
use psutil::process::{MemoryInfo, Process};
use regex::Regex;
use serde::Serialize;
use serde_json::json;
use std::env;
use pmc::{
config,
file::{self, Exists},
helpers::{self, ColoredString},
log,
process::{
http::{self, LogResponse},
ItemSingle, Runner,
},
};
use tabled::{
settings::{
object::{Columns, Rows},
style::{BorderColor, Style},
themes::Colorization,
Color, Modify, Rotate, Width,
},
Table, Tabled,
};
#[derive(Clone, Debug)]
pub enum Args {
Id(usize),
Script(String),
}
fn format(server_name: &String) -> (String, String) {
let kind = ternary!(server_name == "internal", "", "remote ").to_string();
return (kind, server_name.to_string());
}
pub fn get_version(short: bool) -> String {
return match short {
true => format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
false => format!("{} ({} {}) [{}]", env!("CARGO_PKG_VERSION"), env!("GIT_HASH"), env!("BUILD_DATE"), env!("PROFILE")),
};
}
pub fn start(name: &Option<String>, args: &Option<Args>, watch: &Option<String>, server_name: &String) {
let mut runner = Runner::new();
let config = config::read();
let (kind, list_name) = format(server_name);
match args {
Some(Args::Id(id)) => {
let runner: Runner = Runner::new();
println!("{} Applying {kind}action restartProcess on ({id})", *helpers::SUCCESS);
if *server_name == "internal" {
let mut item = runner.get(*id);
match watch {
Some(path) => item.watch(path),
None => item.disable_watch(),
}
name.as_ref().map(|n| item.rename(n.trim().replace("\n", "")));
item.restart();
log!("process started (id={id})");
} else {
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
match Runner::connect(server_name.clone(), server.clone(), false) {
Some(remote) => {
let mut item = remote.get(*id);
name.as_ref().map(|n| item.rename(n.trim().replace("\n", "")));
item.restart();
}
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
}
println!("{} restarted {kind}({id}) ✓", *helpers::SUCCESS);
list(&string!("default"), &list_name);
}
Some(Args::Script(script)) => {
let name = match name {
Some(name) => string!(name),
None => string!(script.split_whitespace().next().unwrap_or_default()),
};
if *server_name == "internal" {
let pattern = Regex::new(r"(?m)^[a-zA-Z0-9]+(/[a-zA-Z0-9]+)*(\.js|\.ts)?$").unwrap();
if pattern.is_match(script) {
let script = format!("{} {script}", config.runner.node);
runner.start(&name, &script, file::cwd(), watch).save();
} else {
runner.start(&name, script, file::cwd(), watch).save();
}
log!("process created (name={name})");
} else {
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
match Runner::connect(server_name.clone(), server.clone(), false) {
Some(mut remote) => remote.start(&name, script, file::cwd(), watch),
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
}
println!("{} Creating {kind}process with ({name})", *helpers::SUCCESS);
println!("{} {kind}created ({name}) ✓", *helpers::SUCCESS);
list(&string!("default"), &list_name);
}
None => {}
}
}
pub fn stop(id: &usize, server_name: &String) {
let mut runner: Runner = Runner::new();
let (kind, list_name) = format(server_name);
println!("{} Applying {kind}action stopProcess on ({id})", *helpers::SUCCESS);
if *server_name != "internal" {
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
runner = match Runner::connect(server_name.clone(), server.clone(), false) {
Some(remote) => remote,
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
}
runner.get(*id).stop();
println!("{} stopped {kind}({id}) ✓", *helpers::SUCCESS);
log!("process stopped {kind}(id={id})");
list(&string!("default"), &list_name);
}
pub fn remove(id: &usize, server_name: &String) {
let mut runner: Runner = Runner::new();
let (kind, _) = format(server_name);
println!("{} Applying {kind}action removeProcess on ({id})", *helpers::SUCCESS);
if *server_name != "internal" {
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
runner = match Runner::connect(server_name.clone(), server.clone(), false) {
Some(remote) => remote,
None => crashln!("{} Failed to remove (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
}
runner.remove(*id);
println!("{} removed {kind}({id}) ✓", *helpers::SUCCESS);
log!("process removed (id={id})");
}
pub fn info(id: &usize, format: &String, server_name: &String) {
#[derive(Clone, Debug, Tabled)]
struct Info {
#[tabled(rename = "error log path ")]
log_error: String,
#[tabled(rename = "out log path")]
log_out: String,
#[tabled(rename = "cpu percent")]
cpu_percent: String,
#[tabled(rename = "memory usage")]
memory_usage: String,
#[tabled(rename = "path hash")]
hash: String,
#[tabled(rename = "watching")]
watch: String,
#[tabled(rename = "exec cwd")]
path: String,
#[tabled(rename = "script command ")]
command: String,
#[tabled(rename = "script id")]
id: String,
restarts: u64,
uptime: String,
pid: String,
name: String,
status: ColoredString,
}
impl Serialize for Info {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let trimmed_json = json!({
"id": &self.id.trim(),
"pid": &self.pid.trim(),
"name": &self.name.trim(),
"path": &self.path.trim(),
"restarts": &self.restarts,
"watch": &self.watch.trim(),
"watch": &self.hash.trim(),
"uptime": &self.uptime.trim(),
"status": &self.status.0.trim(),
"log_out": &self.log_out.trim(),
"cpu": &self.cpu_percent.trim(),
"command": &self.command.trim(),
"mem": &self.memory_usage.trim(),
"log_error": &self.log_error.trim(),
});
trimmed_json.serialize(serializer)
}
}
let render_info = |data: Vec<Info>| {
let table = Table::new(data.clone())
.with(Rotate::Left)
.with(Style::rounded().remove_horizontals())
.with(Colorization::exact([Color::FG_CYAN], Columns::first()))
.with(BorderColor::filled(Color::FG_BRIGHT_BLACK))
.to_string();
if let Ok(json) = serde_json::to_string(&data[0]) {
match format.as_str() {
"raw" => println!("{:?}", data[0]),
"json" => println!("{json}"),
_ => {
println!("{}\n{table}\n", format!("Describing process with id ({id})").on_bright_white().black());
println!(" {}", format!("Use `pmc logs {id} [--lines <num>]` to display logs").white());
println!(" {}", format!("Use `pmc env {id}` to display environment variables").white());
}
};
};
};
if *server_name == "internal" {
if let Some(home) = home::home_dir() {
let config = config::read().runner;
let mut runner = Runner::new();
let item = runner.process(*id);
let mut memory_usage: Option<MemoryInfo> = None;
let mut cpu_percent: Option<f32> = None;
let path = file::make_relative(&item.path, &home).to_string_lossy().into_owned();
if let Ok(mut process) = Process::new(item.pid as u32) {
memory_usage = process.memory_info().ok();
cpu_percent = process.cpu_percent().ok();
}
let cpu_percent = match cpu_percent {
Some(percent) => format!("{:.2}%", percent),
None => string!("0%"),
};
let memory_usage = match memory_usage {
Some(usage) => helpers::format_memory(usage.rss()),
None => string!("0b"),
};
let status = if item.running {
"online ".green().bold()
} else {
match item.crash.crashed {
true => "crashed ",
false => "stopped ",
}
.red()
.bold()
};
let data = vec![Info {
cpu_percent,
memory_usage,
id: string!(id),
restarts: item.restarts,
name: item.name.clone(),
path: format!("{} ", path),
status: ColoredString(status),
log_out: global!("pmc.logs.out", item.name.as_str()),
log_error: global!("pmc.logs.error", item.name.as_str()),
pid: ternary!(item.running, format!("{}", item.pid), string!("n/a")),
command: format!("{} {} '{}'", config.shell, config.args.join(" "), item.script),
hash: ternary!(item.watch.enabled, format!("{} ", item.watch.hash), string!("none ")),
watch: ternary!(item.watch.enabled, format!("{path}/{} ", item.watch.path), string!("disabled ")),
uptime: ternary!(item.running, format!("{}", helpers::format_duration(item.started)), string!("none")),
}];
render_info(data)
} else {
crashln!("{} Impossible to get your home directory", *helpers::FAIL);
}
} else {
let mut item: Option<(pmc::process::Process, Runner)> = None;
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
item = match Runner::connect(server_name.clone(), server.clone(), false) {
Some(mut remote) => Some((remote.process(*id).clone(), remote)),
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
if let Some((item, remote)) = item {
- let info = http::info(&remote.remote.unwrap(), *id);
+ let remote = remote.remote.unwrap();
+ let info = http::info(&remote, *id);
let path = item.path.to_string_lossy().into_owned();
let status = if item.running {
"online ".green().bold()
} else {
match item.crash.crashed {
true => "crashed ",
false => "stopped ",
}
.red()
.bold()
};
if let Ok(info) = info {
let stats = info.json::<ItemSingle>().unwrap().stats;
let cpu_percent = match stats.cpu_percent {
Some(percent) => format!("{:.2}%", percent),
None => string!("0%"),
};
let memory_usage = match stats.memory_usage {
Some(usage) => helpers::format_memory(usage.rss),
None => string!("0b"),
};
let data = vec![Info {
cpu_percent,
memory_usage,
id: string!(id),
path: path.clone(),
- command: item.script,
restarts: item.restarts,
name: item.name.clone(),
status: ColoredString(status),
- log_out: global!("pmc.logs.out", item.name.as_str()),
- log_error: global!("pmc.logs.error", item.name.as_str()),
+ log_out: format!("{}/{{}}-out.log", remote.config.log_path),
+ log_error: format!("{}/{{}}-error.log", remote.config.log_path),
pid: ternary!(item.running, format!("{}", item.pid), string!("n/a")),
hash: ternary!(item.watch.enabled, format!("{} ", item.watch.hash), string!("none ")),
+ command: format!("{} {} '{}'", remote.config.shell, remote.config.args.join(" "), item.script),
watch: ternary!(item.watch.enabled, format!("{path}/{} ", item.watch.path), string!("disabled ")),
uptime: ternary!(item.running, format!("{}", helpers::format_duration(item.started)), string!("none")),
}];
render_info(data)
}
}
}
}
pub fn logs(id: &usize, lines: &usize, server_name: &String) {
let mut runner: Runner = Runner::new();
if *server_name != "internal" {
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
runner = match Runner::connect(server_name.clone(), server.clone(), false) {
Some(remote) => remote,
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
let item = runner.clone().process(*id).clone();
let log_out = http::logs(&runner.remote.as_ref().unwrap(), *id, "out");
let log_error = http::logs(&runner.remote.as_ref().unwrap(), *id, "error");
println!("{}", format!("Showing last {lines} lines for process [{id}] (change the value with --lines option)").yellow());
if let Ok(logs) = log_error {
let logs = logs.json::<LogResponse>().unwrap().logs;
file::logs_internal(logs, *lines, &item.name, *id, "error", &item.name)
}
if let Ok(logs) = log_out {
let logs = logs.json::<LogResponse>().unwrap().logs;
file::logs_internal(logs, *lines, &item.name, *id, "out", &item.name)
}
} else {
let item = runner.process(*id);
let log_out = global!("pmc.logs.out", item.name.as_str());
let log_error = global!("pmc.logs.error", item.name.as_str());
- if Exists::file(log_error.clone()).unwrap() && Exists::file(log_out.clone()).unwrap() {
+ if Exists::check(&log_error).file() && Exists::check(&log_out).file() {
println!("{}", format!("Showing last {lines} lines for process [{id}] (change the value with --lines option)").yellow());
file::logs(*lines, &log_error, *id, "error", &item.name);
file::logs(*lines, &log_out, *id, "out", &item.name);
} else {
crashln!("{} Logs for process ({id}) not found", *helpers::FAIL);
}
}
}
pub fn env(id: &usize, server_name: &String) {
let mut runner: Runner = Runner::new();
if *server_name != "internal" {
let Some(servers) = config::servers().servers else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
runner = match Runner::connect(server_name.clone(), server.clone(), false) {
Some(remote) => remote,
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
}
}
let item = runner.process(*id);
item.env.iter().for_each(|(key, value)| println!("{}: {}", key, value.green()));
}
pub fn list(format: &String, server_name: &String) {
let render_list = |runner: &mut Runner, internal: bool| {
let mut processes: Vec<ProcessItem> = Vec::new();
#[derive(Tabled, Debug)]
struct ProcessItem {
id: ColoredString,
name: String,
pid: String,
uptime: String,
#[tabled(rename = "↺")]
restarts: String,
status: ColoredString,
cpu: String,
mem: String,
#[tabled(rename = "watching")]
watch: String,
}
impl serde::Serialize for ProcessItem {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let trimmed_json = json!({
"cpu": &self.cpu.trim(),
"mem": &self.mem.trim(),
"id": &self.id.0.trim(),
"pid": &self.pid.trim(),
"name": &self.name.trim(),
"watch": &self.watch.trim(),
"uptime": &self.uptime.trim(),
"status": &self.status.0.trim(),
"restarts": &self.restarts.trim(),
});
trimmed_json.serialize(serializer)
}
}
if runner.is_empty() {
println!("{} Process table empty", *helpers::SUCCESS);
} else {
for (id, item) in runner.items() {
let mut cpu_percent: String = string!("0%");
let mut memory_usage: String = string!("0b");
if internal {
let mut usage_internals: (Option<f32>, Option<MemoryInfo>) = (None, None);
if let Ok(mut process) = Process::new(item.pid as u32) {
usage_internals = (process.cpu_percent().ok(), process.memory_info().ok());
}
cpu_percent = match usage_internals.0 {
Some(percent) => format!("{:.0}%", percent),
None => string!("0%"),
};
memory_usage = match usage_internals.1 {
Some(usage) => helpers::format_memory(usage.rss()),
None => string!("0b"),
};
} else {
let info = http::info(&runner.remote.as_ref().unwrap(), id);
if let Ok(info) = info {
let stats = info.json::<ItemSingle>().unwrap().stats;
cpu_percent = match stats.cpu_percent {
Some(percent) => format!("{:.2}%", percent),
None => string!("0%"),
};
memory_usage = match stats.memory_usage {
Some(usage) => helpers::format_memory(usage.rss),
None => string!("0b"),
};
}
}
let status = if item.running {
"online ".green().bold()
} else {
match item.crash.crashed {
true => "crashed ",
false => "stopped ",
}
.red()
.bold()
};
processes.push(ProcessItem {
status: ColoredString(status),
cpu: format!("{cpu_percent} "),
mem: format!("{memory_usage} "),
restarts: format!("{} ", item.restarts),
name: format!("{} ", item.name.clone()),
id: ColoredString(id.to_string().cyan().bold()),
pid: ternary!(item.running, format!("{} ", item.pid), string!("n/a ")),
watch: ternary!(item.watch.enabled, format!("{} ", item.watch.path), string!("disabled ")),
uptime: ternary!(item.running, format!("{} ", helpers::format_duration(item.started)), string!("none ")),
});
}
let table = Table::new(&processes)
.with(Style::rounded().remove_verticals())
.with(BorderColor::filled(Color::FG_BRIGHT_BLACK))
.with(Colorization::exact([Color::FG_BRIGHT_CYAN], Rows::first()))
.with(Modify::new(Columns::single(1)).with(Width::truncate(35).suffix("... ")))
.to_string();
if let Ok(json) = serde_json::to_string(&processes) {
match format.as_str() {
"raw" => println!("{:?}", processes),
"json" => println!("{json}"),
"default" => println!("{table}"),
_ => {}
};
};
}
};
if let Some(servers) = config::servers().servers {
let mut failed: Vec<(String, String)> = vec![];
if let Some(server) = servers.get(server_name) {
match Runner::connect(server_name.clone(), server.clone(), true) {
Some(mut remote) => render_list(&mut remote, false),
None => println!("{} Failed to fetch (name={server_name}, address={})", *helpers::FAIL, server.address),
}
} else {
if matches!(&**server_name, "internal" | "all") {
println!("{} Internal daemon", *helpers::SUCCESS);
render_list(&mut Runner::new(), true);
} else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL);
}
}
if *server_name == "all" {
for (name, server) in servers {
match Runner::connect(name.clone(), server.clone(), true) {
Some(mut remote) => render_list(&mut remote, false),
None => failed.push((name, server.address)),
}
}
}
if !failed.is_empty() {
println!("{} Failed servers:", *helpers::FAIL);
failed
.iter()
.for_each(|server| println!(" {} {} {}", "-".yellow(), format!("{}", server.0), format!("[{}]", server.1).white()));
}
} else {
render_list(&mut Runner::new(), true);
}
}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 04efa19..1d045d9 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,92 +1,111 @@
pub mod structs;
-use crate::file::{self, Exists};
-use crate::helpers;
+use crate::{
+ file::{self, Exists},
+ helpers,
+ process::RemoteConfig,
+};
use colored::Colorize;
-use macros_rs::{crashln, string};
-use std::fs;
+use macros_rs::{crashln, fmtstr, string};
+use reqwest::blocking::Client;
+use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
+use std::fs::write;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use structs::{Config, Daemon, Runner, Secure, Servers, Web};
+pub fn from(address: &str, token: Option<&str>) -> Result<RemoteConfig, anyhow::Error> {
+ let client = Client::new();
+ let mut headers = HeaderMap::new();
+
+ if let Some(token) = token {
+ headers.insert(AUTHORIZATION, HeaderValue::from_static(fmtstr!("token {token}")));
+ }
+
+ let response = client.get(fmtstr!("{address}/daemon/config")).headers(headers).send()?;
+ let json = response.json::<RemoteConfig>()?;
+
+ Ok(json)
+}
+
pub fn read() -> Config {
match home::home_dir() {
Some(path) => {
let path = path.display();
let config_path = format!("{path}/.pmc/config.toml");
- if !Exists::file(config_path.clone()).unwrap() {
+ if !Exists::check(&config_path).file() {
let config = Config {
runner: Runner {
shell: string!("bash"),
args: vec![string!("-c")],
node: string!("node"),
log_path: format!("{path}/.pmc/logs"),
},
daemon: Daemon {
restarts: 10,
interval: 1000,
kind: string!("default"),
web: Web {
ui: false,
api: false,
address: string!("0.0.0.0"),
path: None,
port: 5630,
secure: Secure { enabled: false, token: string!("") },
},
},
};
let contents = match toml::to_string(&config) {
Ok(contents) => contents,
Err(err) => crashln!("{} Cannot parse config.\n{}", *helpers::FAIL, string!(err).white()),
};
- if let Err(err) = fs::write(&config_path, contents) {
+ if let Err(err) = write(&config_path, contents) {
crashln!("{} Error writing config.\n{}", *helpers::FAIL, string!(err).white())
}
log::info!("created config file");
}
file::read(config_path)
}
None => crashln!("{} Impossible to get your home directory", *helpers::FAIL),
}
}
pub fn servers() -> Servers {
match home::home_dir() {
Some(path) => {
let path = path.display();
let config_path = format!("{path}/.pmc/servers.toml");
- if !Exists::file(config_path.clone()).unwrap() {
- if let Err(err) = fs::write(&config_path, "") {
+ if !Exists::check(&config_path).file() {
+ if let Err(err) = write(&config_path, "") {
crashln!("{} Error writing servers.\n{}", *helpers::FAIL, string!(err).white())
}
}
file::read(config_path)
}
None => crashln!("{} Impossible to get your home directory", *helpers::FAIL),
}
}
impl Config {
pub fn get_address(&self) -> SocketAddr {
let config_split: Vec<u8> = match self.daemon.web.address.as_str() {
"localhost" => vec![127, 0, 0, 1],
_ => self.daemon.web.address.split('.').map(|part| part.parse().expect("Failed to parse address part")).collect(),
};
let ipv4_address: Ipv4Addr = Ipv4Addr::from([config_split[0], config_split[1], config_split[2], config_split[3]]);
let ip_address: IpAddr = IpAddr::from(ipv4_address);
let port = self.daemon.web.port as u16;
(ip_address, port).into()
}
pub fn get_path(&self) -> String { self.daemon.web.path.clone().unwrap_or(string!("/")) }
}
diff --git a/src/daemon/api/docs/index.html b/src/daemon/api/docs/index.html
index 3af248e..28e43f3 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="false"
+ allow-authentication="true"
allow-server-selection="false"
theme="dark"
bg-color="#18181b"
primary-color="#0ea5e9"
regular-font="Inter var"
mono-font="Menlo"
/>
</body>
</html>
diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs
index 1cb5737..229e07f 100644
--- a/src/daemon/api/mod.rs
+++ b/src/daemon/api/mod.rs
@@ -1,219 +1,224 @@
mod routes;
use crate::webui;
use bytes::Bytes;
use lazy_static::lazy_static;
use macros_rs::{crashln, 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::Serialize;
use serde_json::json;
use static_dir::static_dir;
use std::{convert::Infallible, str::FromStr};
use utoipa::{OpenApi, ToSchema};
use utoipa_rapidoc::RapiDoc;
use warp::{
body, filters, get, header,
http::{StatusCode, Uri},
path, post, redirect, reject,
reply::{self, html, json},
serve, Filter, Rejection, Reply,
};
#[derive(Serialize, ToSchema)]
struct ErrorMessage {
#[schema(example = 404)]
code: u16,
#[schema(example = "NOT_FOUND")]
message: String,
}
#[inline]
async fn convert_to_string(bytes: Bytes) -> Result<String, Rejection> { String::from_utf8(bytes.to_vec()).map_err(|_| reject()) }
#[inline]
fn string_filter(limit: u64) -> impl Filter<Extract = (String,), Error = Rejection> + Clone { body::content_length_limit(limit).and(body::bytes()).and_then(convert_to_string) }
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();
}
pub async fn start(webui: bool) {
const DOCS: &str = include_str!("docs/index.html");
let config = config::read().daemon.web;
let s_path = config::read().get_path();
let docs_path = fmtstr!("{}/docs.json", s_path.trim_end_matches('/').to_string());
let auth = header::exact("authorization", fmtstr!("token {}", config.secure.token));
- let tmpl =
- match webui::create_template_filter() {
- Ok(template) => template,
- Err(err) => crashln!("{err}"),
- };
+ let tmpl = match webui::create_template_filter() {
+ Ok(template) => template,
+ Err(err) => crashln!("{err}"),
+ };
#[derive(OpenApi)]
#[openapi(
+ security((), ("token" = [])),
paths(
routes::action_handler,
routes::env_handler,
routes::info_handler,
routes::dump_handler,
+ routes::config_handler,
routes::list_handler,
routes::log_handler,
routes::log_handler_raw,
routes::metrics_handler,
routes::prometheus_handler,
routes::create_handler,
routes::rename_handler
),
components(schemas(
ErrorMessage,
process::Log,
process::Raw,
process::Info,
process::Stats,
process::Watch,
process::ItemSingle,
process::ProcessItem,
routes::Stats,
routes::Daemon,
routes::Version,
routes::ActionBody,
+ routes::ConfigBody,
routes::CreateBody,
routes::MetricsRoot,
routes::LogResponse,
routes::DocMemoryInfo,
routes::ActionResponse,
))
)]
struct ApiDoc;
- let app_dump = path!("dump").and(get()).and_then(routes::dump_handler);
- let app_metrics = path!("metrics").and(get()).and_then(routes::metrics_handler);
- let app_prometheus = path!("prometheus").and(get()).and_then(routes::prometheus_handler);
- let app_docs_json = path!("docs.json").and(get()).map(|| json(&ApiDoc::openapi()));
- let app_docs = path!("docs").and(get()).map(|| html(RapiDoc::new(docs_path).custom_html(DOCS).to_html()));
+ let daemon_dump = path!("daemon" / "dump").and(get()).and_then(routes::dump_handler);
+ let daemon_config = path!("daemon" / "config").and(get()).and_then(routes::config_handler);
+ let daemon_metrics = path!("daemon" / "metrics").and(get()).and_then(routes::metrics_handler);
+ let daemon_prometheus = path!("daemon" / "prometheus").and(get()).and_then(routes::prometheus_handler);
+
+ let docs_json = path!("docs.json").and(get()).map(|| json(&ApiDoc::openapi()));
+ let docs_view = path!("docs").and(get()).map(|| html(RapiDoc::new(docs_path).custom_html(DOCS).to_html()));
let process_list = path!("list").and(get()).and_then(routes::list_handler);
let process_env = path!("process" / usize / "env").and(get()).and_then(routes::env_handler);
let process_info = path!("process" / usize / "info").and(get()).and_then(routes::info_handler);
let process_logs = path!("process" / usize / "logs" / String).and(get()).and_then(routes::log_handler);
let process_raw_logs = path!("process" / usize / "logs" / String / "raw").and(get()).and_then(routes::log_handler_raw);
let process_create = path!("process" / "create").and(post()).and(body::json()).and_then(routes::create_handler);
let process_action = path!("process" / usize / "action").and(post()).and(body::json()).and_then(routes::action_handler);
let process_rename = path!("process" / usize / "rename").and(post()).and(string_filter(1024 * 16)).and_then(routes::rename_handler);
let web_login = warp::get().and(path!("login")).and(tmpl.clone()).and_then(routes::login);
let web_dashboard = warp::get().and(path::end()).and(tmpl.clone()).and_then(routes::dashboard);
let web_view_process = warp::get().and(path!("view" / usize)).and(tmpl.clone()).and_then(routes::view_process);
let log = warp::log::custom(|info| {
log!(
"[api] {} (method={}, status={}, ms={:?}, ver={:?})",
info.path(),
info.method(),
info.status().as_u16(),
info.elapsed(),
info.version()
)
});
let base = s_path
.split('/')
.enumerate()
.filter(|(_, p)| !p.is_empty() || *p == s_path)
.fold(warp::any().boxed(), |f, (_, path)| f.and(warp::path(path.to_owned())).boxed());
let routes = process_list
.or(process_env)
.or(process_info)
.or(process_logs)
.or(process_raw_logs)
.or(process_create)
.or(process_action)
.or(process_rename)
- .or(app_metrics)
- .or(app_prometheus)
- .or(app_dump);
+ .or(daemon_dump)
+ .or(daemon_config)
+ .or(daemon_metrics)
+ .or(daemon_prometheus);
let use_routes_basic = || async {
let base_route = path::end().map(|| json(&json!({"healthy": true})).into_response());
let internal = match config.secure.enabled {
- true => routes.clone().and(auth).or(root_redirect()).or(base_route).or(app_docs_json).or(app_docs).boxed(),
- false => routes.clone().or(root_redirect()).or(base_route).or(app_docs_json).or(app_docs).boxed(),
+ true => routes.clone().and(auth).or(root_redirect()).or(base_route).or(docs_json).or(docs_view).boxed(),
+ false => routes.clone().or(root_redirect()).or(base_route).or(docs_json).or(docs_view).boxed(),
};
serve(base.clone().and(internal).recover(handle_rejection).with(log)).run(config::read().get_address()).await
};
let use_routes_web = || async {
let web_routes = web_login.or(web_dashboard).or(web_view_process).or(static_dir!("src/webui/assets"));
let internal = match config.secure.enabled {
- true => routes.clone().and(auth).or(root_redirect()).or(web_routes).or(app_docs_json).or(app_docs).boxed(),
- false => routes.clone().or(root_redirect()).or(web_routes).or(app_docs_json).or(app_docs).boxed(),
+ true => routes.clone().and(auth).or(root_redirect()).or(web_routes).or(docs_json).or(docs_view).boxed(),
+ false => routes.clone().or(root_redirect()).or(web_routes).or(docs_json).or(docs_view).boxed(),
};
serve(base.clone().and(internal).recover(handle_rejection).with(log)).run(config::read().get_address()).await
};
match webui {
true => use_routes_web().await,
false => use_routes_basic().await,
}
}
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let code;
let message;
HTTP_COUNTER.inc();
if err.is_not_found() {
code = StatusCode::NOT_FOUND;
message = "NOT_FOUND";
} else if let Some(_) = err.find::<reject::MissingHeader>() {
code = StatusCode::UNAUTHORIZED;
message = "UNAUTHORIZED";
} else if let Some(_) = err.find::<reject::InvalidHeader>() {
code = StatusCode::UNAUTHORIZED;
message = "UNAUTHORIZED";
} else if let Some(_) = err.find::<reject::MethodNotAllowed>() {
code = StatusCode::METHOD_NOT_ALLOWED;
message = "METHOD_NOT_ALLOWED";
} else {
log!("[api] unhandled rejection (err={:?})", err);
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "INTERNAL_SERVER_ERROR";
}
let json = json(&ErrorMessage {
code: code.as_u16(),
message: message.into(),
});
Ok(reply::with_status(json, code))
}
fn root_redirect() -> filters::BoxedFilter<(impl Reply,)> {
warp::path::full()
.and_then(move |path: path::FullPath| async move {
let path = path.as_str();
if path.ends_with("/") || path.contains(".") {
return Err(warp::reject());
}
Ok(redirect::redirect(Uri::from_str(&[path, "/"].concat()).unwrap()))
})
.boxed()
}
diff --git a/src/daemon/api/routes.rs b/src/daemon/api/routes.rs
index 8cca5c5..06634bd 100644
--- a/src/daemon/api/routes.rs
+++ b/src/daemon/api/routes.rs
@@ -1,465 +1,502 @@
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, Serialize};
use serde_json::json;
use std::convert::Infallible;
use tera::{Context, Tera};
use utoipa::ToSchema;
use pmc::{
- file, helpers,
+ config, file, helpers,
process::{dump, Runner},
};
use crate::daemon::{
api::{HTTP_COUNTER, HTTP_REQ_HISTOGRAM},
pid,
};
use warp::{
hyper::body::Body,
reject,
reply::{self, json, Response},
Rejection, Reply,
};
use std::{
env,
fs::{self, File},
io::{self, BufRead, BufReader},
path::PathBuf,
};
#[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<'a> {
#[schema(example = true)]
done: bool,
#[schema(example = "name")]
action: &'a str,
}
#[derive(Serialize, ToSchema)]
pub(crate) struct LogResponse {
logs: Vec<String>,
}
#[derive(Serialize, ToSchema)]
pub struct MetricsRoot<'a> {
pub version: Version<'a>,
pub daemon: Daemon,
}
#[derive(Serialize, ToSchema)]
pub struct Version<'a> {
#[schema(example = "v1.0.0")]
pub pkg: String,
pub hash: &'a str,
#[schema(example = "2000-01-01")]
pub build_date: &'a str,
#[schema(example = "release")]
pub target: &'a 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,
}
#[inline]
fn attempt(done: bool, method: &str) -> reply::Json {
let data = json!(ActionResponse {
done,
action: ternary!(done, method, "DOES_NOT_EXIST")
});
json(&data)
}
#[inline]
fn render(name: &str, tmpl: &Tera, ctx: &Context) -> Result<String, Rejection> { tmpl.render(name, &ctx).or(Err(reject::not_found())) }
#[inline]
pub async fn login(store: (Tera, String)) -> Result<Box<dyn Reply>, Rejection> {
let mut ctx = Context::new();
let (tmpl, path) = store;
ctx.insert("base_path", &path);
let payload = render("login", &tmpl, &ctx)?;
Ok(Box::new(reply::html(payload)))
}
#[inline]
pub async fn dashboard(store: (Tera, String)) -> Result<Box<dyn Reply>, Rejection> {
let mut ctx = Context::new();
let (tmpl, path) = store;
ctx.insert("base_path", &path);
let payload = render("dashboard", &tmpl, &ctx)?;
Ok(Box::new(reply::html(payload)))
}
#[inline]
pub async fn view_process(id: usize, store: (Tera, String)) -> Result<Box<dyn Reply>, Rejection> {
let mut ctx = Context::new();
let (tmpl, path) = store;
ctx.insert("base_path", &path);
ctx.insert("process_id", &id);
let payload = render("view", &tmpl, &ctx)?;
Ok(Box::new(reply::html(payload)))
}
#[inline]
-#[utoipa::path(get, tag = "Daemon", path = "/prometheus", responses((status = 200, description = "Get prometheus metrics", body = String)))]
+#[utoipa::path(get, tag = "Daemon", path = "/daemon/prometheus",
+ responses((status = 200, description = "Get prometheus metrics", body = String))
+)]
pub async fn prometheus_handler() -> Result<impl Reply, Infallible> {
let encoder = TextEncoder::new();
let mut buffer = Vec::<u8>::new();
let metric_families = prometheus::gather();
encoder.encode(&metric_families, &mut buffer).unwrap();
Ok(format!("{}", String::from_utf8(buffer.clone()).unwrap()))
}
#[inline]
-#[utoipa::path(get, path = "/dump", tag = "Process", responses((status = 200, description = "Dump processes successfully", body = [u8])))]
+#[utoipa::path(get, tag = "Daemon", path = "/daemon/dump", security(()),
+ responses((status = 200, description = "Dump processes successfully", body = [u8]))
+)]
pub async fn dump_handler() -> Result<impl Reply, Infallible> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["dump"]).start_timer();
HTTP_COUNTER.inc();
timer.observe_duration();
Ok(dump::raw())
}
#[inline]
-#[utoipa::path(get, path = "/list", tag = "Process", responses((status = 200, description = "List processes successfully", body = [ProcessItem])))]
+#[utoipa::path(get, tag = "Daemon", path = "/daemon/config",
+ responses((status = 200, description = "Get daemon config successfully", body = ConfigBody))
+)]
+pub async fn config_handler() -> Result<impl Reply, Infallible> {
+ let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["dump"]).start_timer();
+ let config = config::read().runner;
+
+ HTTP_COUNTER.inc();
+
+ let response = json!(ConfigBody {
+ shell: config.shell,
+ args: config.args,
+ log_path: config.log_path,
+ });
+
+ timer.observe_duration();
+ Ok(json(&response))
+}
+
+#[inline]
+#[utoipa::path(get, path = "/list", tag = "Process",
+ responses((status = 200, description = "List processes successfully", body = [ProcessItem]))
+)]
pub async fn list_handler() -> Result<impl Reply, Infallible> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["list"]).start_timer();
let data = Runner::new().json();
HTTP_COUNTER.inc();
timer.observe_duration();
Ok(json(&data))
}
#[inline]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/logs/{kind}",
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)
)
)]
pub async fn log_handler(id: usize, kind: String) -> Result<impl Reply, Rejection> {
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" => global!("pmc.logs.out", item.name.as_str()),
"error" | "stderr" => global!("pmc.logs.error", item.name.as_str()),
_ => global!("pmc.logs.out", item.name.as_str()),
};
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(&json!(LogResponse { logs })))
}
Err(_) => Ok(json(&json!(LogResponse { logs: vec![] }))),
}
}
None => {
timer.observe_duration();
Err(reject::not_found())
}
}
}
#[inline]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/logs/{kind}/raw",
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 = NOT_FOUND, description = "Process was not found", body = ErrorMessage)
)
)]
pub async fn log_handler_raw(id: usize, kind: String) -> Result<impl Reply, Rejection> {
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" => global!("pmc.logs.out", item.name.as_str()),
"error" | "stderr" => global!("pmc.logs.error", item.name.as_str()),
_ => global!("pmc.logs.out", item.name.as_str()),
};
let data = match fs::read_to_string(log_file) {
Ok(data) => data,
Err(err) => err.to_string(),
};
timer.observe_duration();
Ok(Response::new(Body::from(data)))
}
None => {
timer.observe_duration();
Err(reject::not_found())
}
}
}
#[inline]
#[utoipa::path(get, tag = "Process", path = "/process/{id}/info",
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)
)
)]
pub async fn info_handler(id: usize) -> Result<impl Reply, Rejection> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["info"]).start_timer();
let runner = Runner::new();
let mut item = runner.get(id);
HTTP_COUNTER.inc();
timer.observe_duration();
Ok(json(&item.json()))
}
#[inline]
#[utoipa::path(post, tag = "Process", path = "/process/create", request_body(content = CreateBody),
responses(
(status = 200, description = "Create process successful", body = ActionResponse),
(status = INTERNAL_SERVER_ERROR, description = "Failed to create process", body = ErrorMessage)
)
)]
pub async fn create_handler(body: CreateBody) -> Result<impl Reply, Rejection> {
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, &body.watch).save();
timer.observe_duration();
Ok(attempt(true, "create"))
}
#[inline]
#[utoipa::path(post, tag = "Process", path = "/process/{id}/rename", request_body(content = String),
params(("id" = usize, Path, description = "Process id to rename", example = 0)),
responses(
(status = 200, description = "Rename process successful", body = ActionResponse),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage)
)
)]
pub async fn rename_handler(id: usize, body: String) -> Result<impl Reply, Rejection> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["rename"]).start_timer();
let mut runner = Runner::new();
let process = runner.clone().process(id).clone();
HTTP_COUNTER.inc();
if runner.exists(id) {
let mut item = runner.get(id);
item.rename(body.trim().replace("\n", ""));
then!(process.running, item.restart());
timer.observe_duration();
Ok(attempt(true, "rename"))
} else {
timer.observe_duration();
Err(reject::not_found())
}
}
#[inline]
#[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>),
(status = NOT_FOUND, description = "Process was not found", body = ErrorMessage)
)
)]
pub async fn env_handler(id: usize) -> Result<impl Reply, Rejection> {
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(reject::not_found())
}
}
}
#[inline]
#[utoipa::path(post, tag = "Process", path = "/process/{id}/action", request_body = ActionBody,
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)
)
)]
pub async fn action_handler(id: usize, body: ActionBody) -> Result<impl Reply, Rejection> {
let timer = HTTP_REQ_HISTOGRAM.with_label_values(&["action"]).start_timer();
let mut runner = Runner::new();
let method = body.method.as_str();
HTTP_COUNTER.inc();
match method {
"start" | "restart" => {
runner.get(id).restart();
timer.observe_duration();
Ok(attempt(true, method))
}
"stop" | "kill" => {
runner.get(id).stop();
timer.observe_duration();
Ok(attempt(true, method))
}
"remove" | "delete" => {
runner.remove(id);
timer.observe_duration();
Ok(attempt(true, method))
}
_ => {
timer.observe_duration();
Err(reject::not_found())
}
}
}
#[inline]
-#[utoipa::path(get, tag = "Daemon", path = "/metrics", responses((status = 200, description = "Get daemon metrics", body = MetricsRoot)))]
+#[utoipa::path(get, tag = "Daemon", path = "/daemon/metrics",
+ responses((status = 200, description = "Get daemon metrics", body = MetricsRoot))
+)]
pub async fn metrics_handler() -> Result<impl Reply, Infallible> {
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 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"),
};
let response = 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: pid,
running: pid::exists(),
uptime: uptime,
process_count: runner.count(),
daemon_type: global!("pmc.daemon.kind"),
stats: Stats { memory_usage, cpu_percent }
}
});
timer.observe_duration();
Ok(json(&response))
}
diff --git a/src/daemon/log.rs b/src/daemon/log.rs
index 8429643..783c793 100644
--- a/src/daemon/log.rs
+++ b/src/daemon/log.rs
@@ -1,23 +1,24 @@
use chrono::Local;
use global_placeholders::global;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
pub struct Logger {
file: File,
}
impl Logger {
pub fn new() -> io::Result<Self> {
let file = OpenOptions::new().create(true).append(true).open(global!("pmc.daemon.log"))?;
Ok(Logger { file })
}
pub fn write(&mut self, message: &str) {
+ log::info!("{message}");
writeln!(&mut self.file, "[{}] {}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), message).unwrap()
}
}
#[macro_export]
macro_rules! log { ($message:expr $(, $arg:expr)*) =>
{ crate::daemon::log::Logger::new().unwrap().write(format!($message $(, $arg)*).as_str()) }}
diff --git a/src/daemon/pid.rs b/src/daemon/pid.rs
index 7ce4671..7bed24c 100644
--- a/src/daemon/pid.rs
+++ b/src/daemon/pid.rs
@@ -1,65 +1,69 @@
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use global_placeholders::global;
use macros_rs::crashln;
-use pmc::helpers;
+use pmc::{file::Exists, helpers};
use std::{fs, io};
pub fn exists() -> bool { fs::metadata(global!("pmc.pid")).is_ok() }
pub fn running(pid: i32) -> bool { unsafe { libc::kill(pid, 0) == 0 } }
pub fn uptime() -> io::Result<DateTime<Utc>> {
let metadata = fs::metadata(global!("pmc.pid"))?;
let creation_time = metadata.created()?;
let creation_time = DateTime::from(creation_time);
Ok(creation_time)
}
pub fn read() -> Result<i32> {
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::<i32>().map_err(|err| anyhow!(err))?;
Ok(parsed_pid)
}
pub fn write(pid: u32) {
if let Err(err) = fs::write(global!("pmc.pid"), pid.to_string()) {
crashln!("{} Failed to write PID to file: {}", *helpers::FAIL, err);
}
}
pub fn remove() {
- log::debug!("Stale PID file detected. Removing the PID file.");
- if let Err(err) = fs::remove_file(global!("pmc.pid")) {
- crashln!("{} Failed to remove PID file: {}", *helpers::FAIL, err);
+ if Exists::check(&global!("pmc.pid")).file() {
+ log::warn!("Stale PID file detected. Removing the PID file.");
+ if let Err(err) = fs::remove_file(global!("pmc.pid")) {
+ crashln!("{} Failed to remove PID file: {}", *helpers::FAIL, err);
+ }
+ } else {
+ log::info!("No Stale PID file detected.");
}
}
#[cfg(target_os = "linux")]
pub fn name(new_name: &str) {
use std::ffi::CString;
let pid = std::process::id() as libc::pid_t;
let name_cstr = CString::new(new_name).expect("Failed to convert name to CString");
unsafe {
libc::setpgid(pid, 0);
libc::prctl(libc::PR_SET_NAME, name_cstr.as_ptr() as libc::c_ulong, 0, 0, 0);
}
}
#[cfg(target_os = "macos")]
pub fn name(new_name: &str) {
use std::ffi::CString;
let pid = std::process::id() as libc::pid_t;
let name_cstr = CString::new(new_name).expect("Failed to convert name to CString");
unsafe {
libc::setpgid(pid, 0);
libc::pthread_setname_np(name_cstr.as_ptr());
}
}
diff --git a/src/file.rs b/src/file.rs
index 7d855f2..ea27250 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -1,114 +1,117 @@
use crate::{helpers, log};
-use anyhow::Error;
use colored::Colorize;
-use macros_rs::{crashln, str, string, ternary};
+use macros_rs::{crashln, string, ternary};
use std::{
env,
fs::{self, File},
io::{self, BufRead, BufReader},
path::{Path, PathBuf},
thread::sleep,
time::Duration,
};
pub fn logs(lines_to_tail: usize, log_file: &str, id: usize, log_type: &str, item_name: &str) {
let file = File::open(log_file).unwrap();
let reader = BufReader::new(file);
let lines: Vec<String> = reader.lines().collect::<io::Result<_>>().unwrap();
logs_internal(lines, lines_to_tail, log_file, id, log_type, item_name)
}
pub fn logs_internal(lines: Vec<String>, lines_to_tail: usize, log_file: &str, id: usize, log_type: &str, item_name: &str) {
let color = ternary!(log_type == "out", "green", "red");
println!("{}", format!("\n{log_file} last {lines_to_tail} lines:").bright_black());
let start_index = if lines.len() > lines_to_tail { lines.len() - lines_to_tail } else { 0 };
for (_, line) in lines.iter().skip(start_index).enumerate() {
println!("{} {}", format!("{}|{} |", id, item_name).color(color), line);
}
}
pub fn cwd() -> PathBuf {
match env::current_dir() {
Ok(path) => path,
Err(_) => crashln!("{} Unable to find current working directory", *helpers::FAIL),
}
}
pub fn make_relative(current: &Path, home: &Path) -> PathBuf {
match current.strip_prefix(home) {
Err(_) => Path::new(home).join(current),
Ok(relative_path) => Path::new("~").join(relative_path),
}
}
-pub struct Exists;
-impl Exists {
- pub fn folder(dir_name: String) -> Result<bool, Error> { Ok(Path::new(str!(dir_name)).is_dir()) }
- pub fn file(file_name: String) -> Result<bool, Error> { Ok(Path::new(str!(file_name)).exists()) }
+pub struct Exists<'p> {
+ path: &'p str,
+}
+
+impl<'p> Exists<'p> {
+ pub fn check(path: &'p str) -> Self { Self { path } }
+ pub fn folder(&self) -> bool { Path::new(self.path).is_dir() }
+ pub fn file(&self) -> bool { Path::new(self.path).exists() }
}
pub fn raw(path: String) -> Vec<u8> {
match fs::read(&path) {
Ok(contents) => contents,
Err(err) => crashln!("{} Cannot find dumpfile.\n{}", *helpers::FAIL, string!(err).white()),
}
}
pub fn read<T: serde::de::DeserializeOwned>(path: String) -> T {
let contents = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) => crashln!("{} Cannot find dumpfile.\n{}", *helpers::FAIL, string!(err).white()),
};
match toml::from_str(&contents).map_err(|err| string!(err)) {
Ok(parsed) => parsed,
Err(err) => crashln!("{} Cannot parse dumpfile.\n{}", *helpers::FAIL, err.white()),
}
}
pub fn from_rmp<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> T {
match rmp_serde::from_slice(&bytes) {
Ok(parsed) => parsed,
Err(err) => crashln!("{} Cannot parse file.\n{}", *helpers::FAIL, string!(err).white()),
}
}
pub fn read_rmp<T: serde::de::DeserializeOwned>(path: String) -> T {
let mut retry_count = 0;
let max_retries = 5;
let bytes = loop {
match fs::read(&path) {
Ok(contents) => break contents,
Err(err) => {
retry_count += 1;
if retry_count >= max_retries {
log!("{} Cannot find file.\n{}", *helpers::FAIL, string!(err).white());
} else {
log!("{} Error reading file. Retrying... (Attempt {}/{})", *helpers::FAIL, retry_count, max_retries);
}
}
}
sleep(Duration::from_secs(1));
};
retry_count = 0;
loop {
match rmp_serde::from_slice(&bytes) {
Ok(parsed) => break parsed,
Err(err) => {
retry_count += 1;
if retry_count >= max_retries {
log!("{} Cannot parse file.\n{}", *helpers::FAIL, string!(err).white());
} else {
log!("{} Error parsing file. Retrying... (Attempt {}/{})", *helpers::FAIL, retry_count, max_retries);
}
}
}
sleep(Duration::from_secs(1));
}
}
diff --git a/src/globals.rs b/src/globals.rs
index 55bd4ca..84a2844 100644
--- a/src/globals.rs
+++ b/src/globals.rs
@@ -1,39 +1,37 @@
use global_placeholders::init;
use macros_rs::crashln;
-use pmc::config;
-use pmc::file::Exists;
-use pmc::helpers;
+use pmc::{config, file::Exists, helpers};
use std::fs;
pub fn init() {
match home::home_dir() {
Some(path) => {
let path = path.display();
- if !Exists::folder(format!("{path}/.pmc/")).unwrap() {
+ if !Exists::check(&format!("{path}/.pmc/")).folder() {
fs::create_dir_all(format!("{path}/.pmc/")).unwrap();
log::info!("created pmc base dir");
}
let config = config::read();
- if !Exists::folder(config.runner.log_path.clone()).unwrap() {
+ if !Exists::check(&config.runner.log_path).folder() {
fs::create_dir_all(&config.runner.log_path).unwrap();
log::info!("created pmc log dir");
}
init!("pmc.base", format!("{path}/.pmc/"));
init!("pmc.log", format!("{path}/.pmc/pmc.log"));
init!("pmc.pid", format!("{path}/.pmc/daemon.pid"));
init!("pmc.dump", format!("{path}/.pmc/process.dump"));
init!("pmc.daemon.kind", config.daemon.kind);
init!("pmc.daemon.log", format!("{path}/.pmc/daemon.log"));
let out = format!("{}/{{}}-out.log", config.runner.log_path);
let error = format!("{}/{{}}-error.log", config.runner.log_path);
init!("pmc.logs.out", out);
init!("pmc.logs.error", error);
}
None => crashln!("{} Impossible to get your home directory", *helpers::FAIL),
}
}
diff --git a/src/log.rs b/src/log.rs
index 2aed2be..42f2773 100644
--- a/src/log.rs
+++ b/src/log.rs
@@ -1,21 +1,24 @@
use chrono::Local;
use global_placeholders::global;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
pub struct Logger {
file: File,
}
impl Logger {
pub fn new() -> io::Result<Self> {
let file = OpenOptions::new().create(true).append(true).open(global!("pmc.log"))?;
Ok(Logger { file })
}
- pub fn write(&mut self, message: &str) { writeln!(&mut self.file, "[{}] {}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), message).unwrap() }
+ pub fn write(&mut self, message: &str) {
+ log::info!("{message}");
+ writeln!(&mut self.file, "[{}] {}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), message).unwrap()
+ }
}
#[macro_export]
macro_rules! log { ($message:expr $(, $arg:expr)*) =>
{ log::Logger::new().unwrap().write(format!($message $(, $arg)*).as_str()) }}
diff --git a/src/main.rs b/src/main.rs
index 980d553..72ab771 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,169 +1,175 @@
mod cli;
mod daemon;
mod globals;
mod webui;
use crate::cli::Args;
use clap::{Parser, Subcommand};
-use clap_verbosity_flag::Verbosity;
+use clap_verbosity_flag::{LogLevel, Verbosity};
use macros_rs::{str, string, then};
fn validate_id_script(s: &str) -> Result<Args, String> {
if let Ok(id) = s.parse::<usize>() {
Ok(Args::Id(id))
} else {
Ok(Args::Script(s.to_owned()))
}
}
+#[derive(Copy, Clone, Debug, Default)]
+struct NoneLevel;
+impl LogLevel for NoneLevel {
+ fn default() -> Option<log::Level> { None }
+}
+
#[derive(Parser)]
#[command(version = str!(cli::get_version(false)))]
struct Cli {
#[command(subcommand)]
command: Commands,
#[clap(flatten)]
- verbose: Verbosity,
+ verbose: Verbosity<NoneLevel>,
}
#[derive(Subcommand)]
enum Daemon {
/// Reset process index
#[command(alias = "clean")]
Reset,
/// Stop daemon
#[command(alias = "kill")]
Stop,
/// Restart daemon
#[command(alias = "restart", alias = "start")]
Restore {
/// Daemon api
#[arg(long)]
api: bool,
/// WebUI using api
#[arg(long)]
webui: bool,
},
/// Check daemon
#[command(alias = "info", alias = "status")]
Health {
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
},
}
// add pmc restore command
#[derive(Subcommand)]
enum Commands {
/// Start/Restart a process
#[command(alias = "restart")]
Start {
/// Process name
#[arg(long)]
name: Option<String>,
#[clap(value_parser = validate_id_script)]
args: Option<Args>,
/// Watch to reload path
#[arg(long)]
watch: Option<String>,
/// Server
#[arg(short, long, default_value_t = string!("internal"))]
server: String,
},
/// Stop/Kill a process
#[command(alias = "kill")]
Stop {
id: usize,
/// Server
#[arg(short, long, default_value_t = string!("internal"))]
server: String,
},
/// Stop then remove a process
#[command(alias = "rm")]
Remove {
id: usize,
/// Server
#[arg(short, long, default_value_t = string!("internal"))]
server: String,
},
/// Get env of a process
#[command(alias = "cmdline")]
Env {
id: usize,
/// Server
#[arg(short, long, default_value_t = string!("internal"))]
server: String,
},
/// Get information of a process
#[command(alias = "info")]
Details {
id: usize,
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
/// Server
#[arg(short, long, default_value_t = string!("internal"))]
server: String,
},
/// List all processes
#[command(alias = "ls")]
List {
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
/// Server
#[arg(short, long, default_value_t = string!("all"))]
server: String,
},
/// Get logs from a process
Logs {
id: usize,
#[arg(long, default_value_t = 15, help = "")]
lines: usize,
/// Server
#[arg(short, long, default_value_t = string!("internal"))]
server: String,
},
/// Daemon management
Daemon {
#[command(subcommand)]
command: Daemon,
},
}
fn main() {
let cli = Cli::parse();
let mut env = env_logger::Builder::new();
let level = cli.verbose.log_level_filter();
globals::init();
env.filter_level(level).init();
match &cli.command {
Commands::Start { name, args, watch, server } => cli::start(name, args, watch, server),
Commands::Stop { id, server } => cli::stop(id, server),
Commands::Remove { id, server } => cli::remove(id, server),
Commands::Env { id, server } => cli::env(id, server),
Commands::Details { id, format, server } => cli::info(id, format, server),
Commands::List { format, server } => cli::list(format, server),
Commands::Logs { id, lines, server } => cli::logs(id, lines, server),
Commands::Daemon { command } => match command {
Daemon::Stop => daemon::stop(),
Daemon::Reset => daemon::reset(),
Daemon::Health { format } => daemon::health(format),
Daemon::Restore { api, webui } => daemon::restart(api, webui, level.as_str() != "ERROR"),
},
};
if !matches!(&cli.command, Commands::Daemon { .. }) {
then!(!daemon::pid::exists(), daemon::start(false));
}
}
diff --git a/src/process/dump.rs b/src/process/dump.rs
index efa87f7..27982cc 100644
--- a/src/process/dump.rs
+++ b/src/process/dump.rs
@@ -1,67 +1,67 @@
use crate::{
file::{self, Exists},
helpers, log,
process::{id::Id, Runner},
};
use colored::Colorize;
use global_placeholders::global;
use macros_rs::{crashln, fmtstr, string};
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use std::{collections::BTreeMap, fs};
-pub fn from<'r>(address: &str, token: Option<&str>) -> Result<Runner, anyhow::Error> {
+pub fn from(address: &str, token: Option<&str>) -> Result<Runner, anyhow::Error> {
let client = Client::new();
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert(AUTHORIZATION, HeaderValue::from_static(fmtstr!("token {token}")));
}
- let response = client.get(fmtstr!("{address}/dump")).headers(headers).send()?;
+ let response = client.get(fmtstr!("{address}/daemon/dump")).headers(headers).send()?;
let bytes = response.bytes()?;
Ok(file::from_rmp(&bytes))
}
pub fn read() -> Runner {
- if !Exists::file(global!("pmc.dump")).unwrap() {
+ if !Exists::check(&global!("pmc.dump")).file() {
let runner = Runner {
id: Id::new(0),
list: BTreeMap::new(),
remote: None,
};
write(&runner);
log!("created dump file");
}
file::read_rmp(global!("pmc.dump"))
}
pub fn raw() -> Vec<u8> {
- if !Exists::file(global!("pmc.dump")).unwrap() {
+ if !Exists::check(&global!("pmc.dump")).file() {
let runner = Runner {
id: Id::new(0),
list: BTreeMap::new(),
remote: None,
};
write(&runner);
log!("created dump file");
}
file::raw(global!("pmc.dump"))
}
pub fn write(dump: &Runner) {
let encoded: Vec<u8> = match rmp_serde::to_vec(&dump) {
Ok(contents) => contents,
Err(err) => crashln!("{} Cannot encode dump.\n{}", *helpers::FAIL, string!(err).white()),
};
if let Err(err) = fs::write(global!("pmc.dump"), encoded) {
crashln!("{} Error writing dumpfile.\n{}", *helpers::FAIL, string!(err).white())
}
}
diff --git a/src/process/hash.rs b/src/process/hash.rs
index a7dfa4e..e6dfb22 100644
--- a/src/process/hash.rs
+++ b/src/process/hash.rs
@@ -1,14 +1,14 @@
use macros_rs::crashln;
use merkle_hash::{bytes_to_hex, Algorithm, MerkleTree};
use std::path::PathBuf;
pub fn create(path: PathBuf) -> String {
- log::debug!("creating hash for {:?}", path);
+ log::info!("creating hash for {:?}", path);
let tree = match MerkleTree::builder(&path.to_str().unwrap()).algorithm(Algorithm::Blake3).hash_names(false).build() {
Ok(v) => v,
Err(e) => crashln!("Invalid UTF-8 sequence: {}", e),
};
log::trace!("hash {:?}", tree.root.item.hash);
bytes_to_hex(tree.root.item.hash)
}
diff --git a/src/process/http.rs b/src/process/http.rs
index e96756c..1e63271 100644
--- a/src/process/http.rs
+++ b/src/process/http.rs
@@ -1,78 +1,78 @@
use crate::process::Remote;
use macros_rs::{fmtstr, string};
use reqwest::blocking::{Client, Response};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize)]
struct ActionBody {
pub method: String,
}
#[derive(Deserialize)]
pub struct LogResponse {
pub logs: 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) {
let client = Client::new();
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert(AUTHORIZATION, HeaderValue::from_static(fmtstr!("token {token}")));
}
return (client, headers);
}
-pub fn info(Remote { address, token }: &Remote, id: usize) -> Result<Response, anyhow::Error> {
+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<Response, anyhow::Error> {
+pub fn logs(Remote { address, token, .. }: &Remote, id: usize, kind: &str) -> Result<Response, anyhow::Error> {
let (client, headers) = client(token);
Ok(client.get(fmtstr!("{address}/process/{id}/logs/{kind}")).headers(headers).send()?)
}
-pub fn create(Remote { address, token }: &Remote, name: &String, script: &String, path: PathBuf, watch: &Option<String>) -> Result<Response, anyhow::Error> {
+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> {
+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> {
+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> {
+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> {
+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 7f2615b..4b50cd3 100644
--- a/src/process/mod.rs
+++ b/src/process/mod.rs
@@ -1,507 +1,523 @@
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 serde_json::{json, Value};
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(Serialize, 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,
}
}
}
impl Runner {
pub fn new() -> Self { dump::read() }
pub fn connect(name: String, Server { address, token }: Server, verbose: bool) -> Option<Self> {
- match dump::from(&address, token.as_deref()) {
- Ok(dump) => {
- then!(verbose, println!("{} Fetched remote (name={name}, address={address})", *helpers::SUCCESS));
- return Some(Runner {
- remote: Some(Remote { token, address: string!(address) }),
- ..dump
- });
- }
+ let remote_config = match config::from(&address, token.as_deref()) {
+ Ok(config) => config,
Err(err) => {
- log::debug!("{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.crash.value = 0;
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(&mut 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(&mut self, id: usize) -> bool { self.list.contains_key(&id) }
pub fn info(&mut 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 json(&mut self) -> Value {
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"),
- }
- };
+ 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),
});
}
json!(processes)
}
}
impl ProcessWrapper {
pub fn stop(&mut self) {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
runner.stop(self.id).save();
}
pub fn watch(&mut self, path: &str) {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
runner.watch(self.id, path, true).save();
}
pub fn disable_watch(&mut self) {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
runner.watch(self.id, "", false).save();
}
pub fn rename(&mut self, name: String) {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
runner.rename(self.id, name).save();
}
pub fn restart(&mut self) {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
runner.restart(self.id, false).save();
}
pub fn crashed(&mut self) {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
runner.new_crash(self.id).save();
runner.restart(self.id, true).save();
}
pub fn json(&mut self) -> Value {
let runner_arc = Arc::clone(&self.runner);
let mut runner = runner_arc.lock().unwrap();
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"),
}
};
json!(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: global!("pmc.logs.out", item.name.as_str()),
error: global!("pmc.logs.error", item.name.as_str()),
},
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/base.astro b/src/webui/src/components/base.astro
index 51188c6..8d64f75 100644
--- a/src/webui/src/components/base.astro
+++ b/src/webui/src/components/base.astro
@@ -1,60 +1,60 @@
---
import '@/styles.css'
import favicon from '@/public/favicon.svg?url'
import banner from '@/public/banner.png?url'
interface Props {
title: string;
description: string;
image?: string;
}
const { title, description } = Astro.props;
---
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<link rel="icon" type="image/svg+xml" href={favicon} />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<app-redirect data-base={"{{base_path | safe}}"} />
<meta name="title" content={title} />
<meta name="description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={banner} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={banner} />
<script>
import { $settings } from '@/store';
class AppRedirect extends HTMLElement {
constructor() {
super();
const base = this.dataset.base;
- fetch(base + '/metrics', {
+ fetch(base + '/daemon/metrics', {
headers: { Authorization: 'token ' + $settings.get().token },
}).then(response => {
if (window.location.pathname.includes("login")) {
if (response.status === 200) {
window.location.href = base + "/";
}
} else {
if (response.status !== 200) {
window.location.href = base + '/login';
}
}
});
}
}
customElements.define('app-redirect', AppRedirect);
</script>
diff --git a/src/webui/src/components/react/login.tsx b/src/webui/src/components/react/login.tsx
index 06cf537..c98597e 100644
--- a/src/webui/src/components/react/login.tsx
+++ b/src/webui/src/components/react/login.tsx
@@ -1,78 +1,78 @@
import { $settings } from '@/store';
import { useState, Fragment } from 'react';
import favicon from '@/public/favicon.svg?url';
const Login = (props: { base: string }) => {
const [token, setToken] = useState('');
const [loginFailed, setLoginFailed] = useState(false);
const handleChange = (event: any) => setToken(event.target.value);
const handleSubmit = (event: any) => {
event.preventDefault();
$settings.setKey('token', token);
- fetch(props.base + '/metrics', {
+ fetch(props.base + '/daemon/metrics', {
headers: { Authorization: 'token ' + token },
}).then((response) => {
if (response.status === 200) {
window.location.href = props.base;
} else {
setLoginFailed(true);
setTimeout(() => {
setLoginFailed(false);
}, 3000);
}
});
};
return (
<Fragment>
{loginFailed && (
<div className="-mb-[92px] sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg bg-red-600 sm:border border-red-400/50 p-4 sm:mt-4 sm:-mb-[110px]">
<h3 className="text-sm font-medium text-white">Failed to login. Is the token correct?</h3>
</div>
)}
<div className="h-screen flex items-center -mt-10">
<div className="flex min-h-full flex-1 flex-col justify-center px-0 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<img className="mx-auto h-10 w-auto" src={`${base}/${favicon}`} alt="PMC" />
<h2 className="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-zinc-200">Provide token to continue</h2>
</div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div className="px-5 py-6 rounded-lg bg-none sm:border border-zinc-700/30 sm:bg-zinc-900/10">
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
<div>
<input
required
id="password"
name="password"
type="password"
value={token}
onChange={handleChange}
placeholder="••••••••••••••••"
autoComplete="current-password"
className="block w-full rounded-md border-0 bg-white/5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm sm:leading-6 placeholder:text-zinc-600"
/>
</div>
</div>
<div>
<button
type="submit"
className="-mb-1 transition flex w-full justify-center rounded-md px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 border focus:outline-none focus:ring-0 focus:ring-offset-0 focus:z-10 shrink-0 saturate-[110%] border-zinc-700 hover:border-zinc-600 bg-zinc-800 text-zinc-50 hover:bg-zinc-700">
Continue
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</Fragment>
);
};
export default Login;

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 1, 6:47 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
494887
Default Alt Text
(105 KB)

Event Timeline