Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2707941
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
55 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/cli/internal.rs b/src/cli/internal.rs
new file mode 100644
index 0000000..1fc9b74
--- /dev/null
+++ b/src/cli/internal.rs
@@ -0,0 +1,131 @@
+use macros_rs::{crashln, string};
+use pmc::{config, file, helpers, log, process::Runner};
+use regex::Regex;
+
+pub struct Internal<'i> {
+ pub id: usize,
+ pub runner: Runner,
+ pub kind: String,
+ pub server_name: &'i String,
+}
+
+impl<'i> Internal<'i> {
+ pub fn create(mut self, script: &String, name: &Option<String>, watch: &Option<String>) {
+ let config = config::read();
+ let name = match name {
+ Some(name) => string!(name),
+ None => string!(script.split_whitespace().next().unwrap_or_default()),
+ };
+
+ if matches!(&**self.server_name, "internal" | "local") {
+ 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);
+ self.runner.start(&name, &script, file::cwd(), watch).save();
+ } else {
+ self.runner.start(&name, script, file::cwd(), watch).save();
+ }
+ } else {
+ let Some(servers) = config::servers().servers else {
+ crashln!("{} Failed to read servers", *helpers::FAIL)
+ };
+
+ if let Some(server) = servers.get(self.server_name) {
+ match Runner::connect(self.server_name.clone(), server.get(), false) {
+ Some(mut remote) => remote.start(&name, script, file::cwd(), watch),
+ None => crashln!("{} Failed to connect (name={}, address={})", *helpers::FAIL, self.server_name, server.address),
+ };
+ } else {
+ crashln!("{} Server '{}' does not exist", *helpers::FAIL, self.server_name,)
+ };
+ }
+
+ println!("{} Creating {}process with ({name})", *helpers::SUCCESS, self.kind);
+ println!("{} {}created ({name}) ✓", *helpers::SUCCESS, self.kind);
+ }
+
+ pub fn restart(self, name: &Option<String>, watch: &Option<String>) {
+ println!("{} Applying {}action restartProcess on ({})", *helpers::SUCCESS, self.kind, self.id);
+
+ if matches!(&**self.server_name, "internal" | "local") {
+ let mut item = self.runner.get(self.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={})", self.id);
+ } else {
+ let Some(servers) = config::servers().servers else {
+ crashln!("{} Failed to read servers", *helpers::FAIL)
+ };
+
+ if let Some(server) = servers.get(self.server_name) {
+ match Runner::connect(self.server_name.clone(), server.get(), false) {
+ Some(remote) => {
+ let mut item = remote.get(self.id);
+
+ name.as_ref().map(|n| item.rename(n.trim().replace("\n", "")));
+ item.restart();
+ }
+ None => crashln!("{} Failed to connect (name={}, address={})", *helpers::FAIL, self.server_name, server.address),
+ }
+ } else {
+ crashln!("{} Server '{}' does not exist", *helpers::FAIL, self.server_name)
+ };
+ }
+
+ println!("{} restarted {}({}) ✓", *helpers::SUCCESS, self.kind, self.id);
+ }
+
+ pub fn stop(mut self) {
+ println!("{} Applying {}action stopProcess on ({})", *helpers::SUCCESS, self.kind, self.id);
+
+ if !matches!(&**self.server_name, "internal" | "local") {
+ let Some(servers) = config::servers().servers else {
+ crashln!("{} Failed to read servers", *helpers::FAIL)
+ };
+
+ if let Some(server) = servers.get(self.server_name) {
+ self.runner = match Runner::connect(self.server_name.clone(), server.get(), false) {
+ Some(remote) => remote,
+ None => crashln!("{} Failed to connect (name={}, address={})", *helpers::FAIL, self.server_name, server.address),
+ };
+ } else {
+ crashln!("{} Server '{}' does not exist", *helpers::FAIL, self.server_name)
+ };
+ }
+
+ self.runner.get(self.id).stop();
+ println!("{} stopped {}({}) ✓", *helpers::SUCCESS, self.kind, self.id);
+ log!("process stopped {}(id={})", self.kind, self.id);
+ }
+
+ pub fn remove(mut self) {
+ println!("{} Applying {}action removeProcess on ({})", *helpers::SUCCESS, self.kind, self.id);
+
+ if !matches!(&**self.server_name, "internal" | "local") {
+ let Some(servers) = config::servers().servers else {
+ crashln!("{} Failed to read servers", *helpers::FAIL)
+ };
+
+ if let Some(server) = servers.get(self.server_name) {
+ self.runner = match Runner::connect(self.server_name.clone(), server.get(), false) {
+ Some(remote) => remote,
+ None => crashln!("{} Failed to remove (name={}, address={})", *helpers::FAIL, self.server_name, server.address),
+ };
+ } else {
+ crashln!("{} Server '{}' does not exist", *helpers::FAIL, self.server_name)
+ };
+ }
+
+ self.runner.remove(self.id);
+ println!("{} removed {}({}) ✓", *helpers::SUCCESS, self.kind, self.id);
+ log!("process removed (id={})", self.id);
+ }
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 5232fb3..fa6b59e 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -1,607 +1,520 @@
+pub(crate) mod internal;
pub(crate) mod server;
use colored::Colorize;
+use internal::Internal;
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,
helpers::{self, ColoredString},
log,
process::{http, 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),
}
+#[derive(Clone, Debug)]
+pub enum Item {
+ Id(usize),
+ Name(String),
+}
+
fn format(server_name: &String) -> (String, String) {
let kind = ternary!(matches!(&**server_name, "internal" | "local"), "", "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 => match env!("GIT_HASH") {
"" => format!("{} ({}) [{}]", env!("CARGO_PKG_VERSION"), env!("BUILD_DATE"), env!("PROFILE")),
hash => format!("{} ({} {hash}) [{}]", env!("CARGO_PKG_VERSION"), 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();
+pub fn start(name: &Option<String>, args: &Args, watch: &Option<String>, server_name: &String) {
+ let runner = Runner::new();
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 matches!(&**server_name, "internal" | "local") {
- 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!("{} Failed to read servers", *helpers::FAIL)
- };
-
- if let Some(server) = servers.get(server_name) {
- match Runner::connect(server_name.clone(), server.get(), 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),
- }
- } else {
- crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
- };
- }
-
- 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 matches!(&**server_name, "internal" | "local") {
- 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!("{} Failed to read servers", *helpers::FAIL)
- };
-
- if let Some(server) = servers.get(server_name) {
- match Runner::connect(server_name.clone(), server.get(), false) {
- Some(mut remote) => remote.start(&name, script, file::cwd(), watch),
- None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
- };
- } else {
- crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
- };
- }
-
- println!("{} Creating {kind}process with ({name})", *helpers::SUCCESS);
-
- println!("{} {kind}created ({name}) ✓", *helpers::SUCCESS);
- list(&string!("default"), &list_name);
- }
- None => {}
+ Args::Id(id) => Internal { id: *id, runner, server_name, kind }.restart(name, watch),
+ Args::Script(script) => match runner.find(&script) {
+ Some(id) => Internal { id, runner, server_name, kind }.restart(name, watch),
+ None => Internal { id: 0, runner, server_name, kind }.create(script, name, watch),
+ },
}
+
+ list(&string!("default"), &list_name);
}
-pub fn stop(id: &usize, server_name: &String) {
- let mut runner: Runner = Runner::new();
+pub fn stop(item: &Item, server_name: &String) {
+ let runner: Runner = Runner::new();
let (kind, list_name) = format(server_name);
- println!("{} Applying {kind}action stopProcess on ({id})", *helpers::SUCCESS);
-
- if !matches!(&**server_name, "internal" | "local") {
- let Some(servers) = config::servers().servers else {
- crashln!("{} Failed to read servers", *helpers::FAIL)
- };
- if let Some(server) = servers.get(server_name) {
- runner = match Runner::connect(server_name.clone(), server.get(), false) {
- Some(remote) => remote,
- None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
- };
- } else {
- crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
- };
+ match item {
+ Item::Id(id) => Internal { id: *id, runner, server_name, kind }.stop(),
+ Item::Name(name) => match runner.find(&name) {
+ Some(id) => Internal { id, runner, server_name, kind }.stop(),
+ None => crashln!("{} Process ({name}) not found", *helpers::FAIL),
+ },
}
- 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();
+pub fn remove(item: &Item, server_name: &String) {
+ let runner: Runner = Runner::new();
let (kind, _) = format(server_name);
- println!("{} Applying {kind}action removeProcess on ({id})", *helpers::SUCCESS);
- if !matches!(&**server_name, "internal" | "local") {
- let Some(servers) = config::servers().servers else {
- crashln!("{} Failed to read servers", *helpers::FAIL)
- };
-
- if let Some(server) = servers.get(server_name) {
- runner = match Runner::connect(server_name.clone(), server.get(), false) {
- Some(remote) => remote,
- None => crashln!("{} Failed to remove (name={server_name}, address={})", *helpers::FAIL, server.address),
- };
- } else {
- crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
- };
+ match item {
+ Item::Id(id) => Internal { id: *id, runner, server_name, kind }.remove(),
+ Item::Name(name) => match runner.find(&name) {
+ Some(id) => Internal { id, runner, server_name, kind }.remove(),
+ None => crashln!("{} Process ({name}) not found", *helpers::FAIL),
+ },
}
-
- 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,
children: 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,
"hash": &self.hash.trim(),
"watch": &self.watch.trim(),
"children": &self.children,
"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 matches!(&**server_name, "internal" | "local") {
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();
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) {
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 {
children,
cpu_percent,
memory_usage,
id: string!(id),
restarts: item.restarts,
name: item.name.clone(),
log_out: item.logs().out,
path: format!("{} ", path),
log_error: item.logs().error,
status: ColoredString(status),
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 data: (pmc::process::Process, Runner);
let Some(servers) = config::servers().servers else {
crashln!("{} Failed to read servers", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
data = match Runner::connect(server_name.clone(), server.get(), false) {
Some(mut remote) => (remote.process(*id).clone(), remote),
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
} else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
let (item, remote) = data;
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 children = if item.children.is_empty() { "none".to_string() } else { format!("{:?}", item.children) };
let cpu_percent = match stats.cpu_percent {
Some(percent) => format!("{percent:.2}%"),
None => string!("0%"),
};
let memory_usage = match stats.memory_usage {
Some(usage) => helpers::format_memory(usage.rss),
None => string!("0b"),
};
let data = vec![Info {
children,
cpu_percent,
memory_usage,
id: string!(id),
path: path.clone(),
status: status.into(),
restarts: item.restarts,
name: item.name.clone(),
pid: ternary!(item.running, format!("{pid}", pid = item.pid), string!("n/a")),
log_out: format!("{}/{}-out.log", remote.config.log_path, item.name),
log_error: format!("{}/{}-error.log", remote.config.log_path, item.name),
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 !matches!(&**server_name, "internal" | "local") {
let Some(servers) = config::servers().servers else {
crashln!("{} Failed to read servers", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
runner = match Runner::connect(server_name.clone(), server.get(), false) {
Some(remote) => remote,
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
} else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
let item = runner.info(*id).unwrap_or_else(|| crashln!("{} Process ({id}) not found", *helpers::FAIL));
println!("{}", format!("Showing last {lines} lines for process [{id}] (change the value with --lines option)").yellow());
for kind in vec!["error", "out"] {
let logs = http::logs(&runner.remote.as_ref().unwrap(), *id, kind);
if let Ok(log) = logs {
if log.lines.is_empty() {
println!("{} No logs found for {}/{kind}", *helpers::FAIL, item.name);
continue;
}
file::logs_internal(log.lines, *lines, log.path, *id, kind, &item.name)
}
}
} else {
let item = runner.info(*id).unwrap_or_else(|| crashln!("{} Process ({id}) not found", *helpers::FAIL));
println!("{}", format!("Showing last {lines} lines for process [{id}] (change the value with --lines option)").yellow());
file::logs(item, *lines, "error");
file::logs(item, *lines, "out");
}
}
pub fn env(id: &usize, server_name: &String) {
let mut runner: Runner = Runner::new();
if !matches!(&**server_name, "internal" | "local") {
let Some(servers) = config::servers().servers else {
crashln!("{} Failed to read servers", *helpers::FAIL)
};
if let Some(server) = servers.get(server_name) {
runner = match Runner::connect(server_name.clone(), server.get(), false) {
Some(remote) => remote,
None => crashln!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address),
};
} else {
crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL)
};
}
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: status.into(),
cpu: format!("{cpu_percent} "),
mem: format!("{memory_usage} "),
id: id.to_string().cyan().bold().into(),
restarts: format!("{} ", item.restarts),
name: format!("{} ", item.name.clone()),
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.get(), 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" | "global" | "local") {
if *server_name == "all" || *server_name == "global" {
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" || *server_name == "global" {
for (name, server) in servers {
match Runner::connect(name.clone(), server.get(), 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/main.rs b/src/main.rs
index f7d2ef2..b48f573 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,222 +1,238 @@
mod cli;
mod daemon;
mod globals;
mod webui;
-use crate::{cli::Args, globals::defaults};
use clap::{Parser, Subcommand};
use clap_verbosity_flag::{LogLevel, Verbosity};
use macros_rs::{str, string, then};
use update_informer::{registry, Check};
+use crate::{
+ cli::{Args, Item},
+ globals::defaults,
+};
+
+// migrate to helpers
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()))
}
}
+// migrate to helpers
+fn validate_item(s: &str) -> Result<Item, String> {
+ if let Ok(id) = s.parse::<usize>() {
+ Ok(Item::Id(id))
+ } else {
+ Ok(Item::Name(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<NoneLevel>,
}
#[derive(Subcommand)]
enum Daemon {
/// Reset process index
#[command(visible_alias = "clean")]
Reset,
/// Stop daemon
#[command(visible_alias = "kill")]
Stop,
/// Restart daemon
#[command(visible_alias = "restart", visible_alias = "start")]
Restore {
/// Daemon api
#[arg(long)]
api: bool,
/// WebUI using api
#[arg(long)]
webui: bool,
},
/// Check daemon
#[command(visible_alias = "info", visible_alias = "status")]
Health {
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
},
}
#[derive(Subcommand)]
enum Server {
/// Add new server
#[command(visible_alias = "add")]
New,
/// List servers
#[command(visible_alias = "ls")]
List {
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
},
/// Remove server
#[command(visible_alias = "rm")]
Remove {
/// Server name
name: String,
},
/// Set default server
#[command(visible_alias = "set")]
Default {
/// Server name
name: Option<String>,
},
}
// add pmc restore command
#[derive(Subcommand)]
enum Commands {
/// Start/Restart a process
#[command(visible_alias = "restart")]
Start {
/// Process name
#[arg(long)]
name: Option<String>,
#[clap(value_parser = validate_id_script)]
- args: Option<Args>,
+ args: Args,
/// Watch to reload path
#[arg(long)]
watch: Option<String>,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// Stop/Kill a process
#[command(visible_alias = "kill")]
Stop {
- id: usize,
+ #[clap(value_parser = validate_item)]
+ item: Item,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// Stop then remove a process
#[command(visible_alias = "rm")]
Remove {
- id: usize,
+ #[clap(value_parser = validate_item)]
+ item: Item,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// Get env of a process
#[command(visible_alias = "cmdline")]
Env {
id: usize,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// Get information of a process
#[command(visible_alias = "info")]
Details {
id: usize,
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// List all processes
#[command(visible_alias = "ls")]
List {
/// Format output
#[arg(long, default_value_t = string!("default"))]
format: String,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// Get logs from a process
Logs {
id: usize,
#[arg(long, default_value_t = 15, help = "")]
lines: usize,
/// Server
#[arg(short, long)]
server: Option<String>,
},
/// Daemon management
#[command(visible_alias = "agent", visible_alias = "bgd")]
Daemon {
#[command(subcommand)]
command: Daemon,
},
/// Server management
#[command(visible_alias = "remote", visible_alias = "srv")]
Server {
#[command(subcommand)]
command: Server,
},
}
fn main() {
let cli = Cli::parse();
let mut env = env_logger::Builder::new();
let level = cli.verbose.log_level_filter();
let informer = update_informer::new(registry::Crates, "pmc", env!("CARGO_PKG_VERSION"));
if let Some(version) = informer.check_version().ok().flatten() {
println!("{} New version is available: {version}", *pmc::helpers::WARN);
}
globals::init();
env.filter_level(level).init();
match &cli.command {
Commands::Start { name, args, watch, server } => cli::start(name, args, watch, &defaults(server)),
- Commands::Stop { id, server } => cli::stop(id, &defaults(server)),
- Commands::Remove { id, server } => cli::remove(id, &defaults(server)),
+ Commands::Stop { item, server } => cli::stop(item, &defaults(server)),
+ Commands::Remove { item, server } => cli::remove(item, &defaults(server)),
Commands::Env { id, server } => cli::env(id, &defaults(server)),
Commands::Details { id, format, server } => cli::info(id, format, &defaults(server)),
Commands::List { format, server } => cli::list(format, &defaults(server)),
Commands::Logs { id, lines, server } => cli::logs(id, lines, &defaults(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() != "OFF"),
},
Commands::Server { command } => match command {
Server::New => cli::server::new(),
Server::Remove { name } => cli::server::remove(name),
Server::Default { name } => cli::server::default(name),
Server::List { format } => cli::server::list(format, cli.verbose.log_level()),
},
};
if !matches!(&cli.command, Commands::Daemon { .. }) && !matches!(&cli.command, Commands::Server { .. }) {
then!(!daemon::pid::exists(), daemon::restart(&false, &false, false));
}
}
diff --git a/src/process/mod.rs b/src/process/mod.rs
index f6fe68a..7743222 100644
--- a/src/process/mod.rs
+++ b/src/process/mod.rs
@@ -1,570 +1,576 @@
mod unix;
use crate::{
config,
config::structs::Server,
file, helpers,
service::{run, stop, ProcessMetadata},
};
use std::{
env,
path::PathBuf,
sync::{Arc, Mutex},
};
use nix::{
sys::signal::{kill, Signal},
unistd::Pid,
};
use chrono::serde::ts_milliseconds;
use chrono::{DateTime, Utc};
use global_placeholders::global;
use macros_rs::{crashln, string, ternary, then};
use psutil::process;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
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,
pub children: Vec<i64>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Stats {
pub restarts: u64,
pub start_time: i64,
pub cpu_percent: Option<f32>,
pub memory_usage: Option<MemoryInfo>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct MemoryInfo {
pub rss: u64,
pub vms: u64,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Log {
pub out: String,
pub error: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Raw {
pub running: bool,
pub crashed: bool,
pub crashes: u64,
}
#[derive(Clone)]
pub struct LogInfo {
pub out: String,
pub error: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ProcessItem {
pid: i64,
id: usize,
cpu: String,
mem: String,
name: String,
restarts: u64,
status: String,
uptime: String,
#[schema(example = "/path")]
watch_path: String,
#[schema(value_type = String, example = "2000-01-01T01:00:00.000Z")]
start_time: DateTime<Utc>,
}
#[derive(Clone)]
pub struct ProcessWrapper {
pub id: usize,
pub runner: Arc<Mutex<Runner>>,
}
type Env = BTreeMap<String, String>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Process {
pub id: usize,
pub pid: i64,
pub env: Env,
pub name: String,
pub path: PathBuf,
pub script: String,
pub restarts: u64,
pub running: bool,
pub crash: Crash,
pub watch: Watch,
pub children: Vec<i64>,
#[serde(with = "ts_milliseconds")]
pub started: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Crash {
pub crashed: bool,
pub value: u64,
}
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct Watch {
pub enabled: bool,
#[schema(example = "/path")]
pub path: String,
pub hash: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Runner {
pub id: id::Id,
#[serde(skip)]
pub remote: Option<Remote>,
pub list: BTreeMap<usize, Process>,
}
#[derive(Clone, Debug)]
pub struct Remote {
address: String,
token: Option<String>,
pub config: RemoteConfig,
}
#[derive(Clone, Debug, Deserialize)]
pub struct RemoteConfig {
pub shell: String,
pub args: Vec<String>,
pub log_path: String,
}
pub enum Status {
Offline,
Running,
}
impl Status {
pub fn to_bool(&self) -> bool {
match self {
Status::Offline => false,
Status::Running => true,
}
}
}
macro_rules! lock {
($runner:expr) => {{
match $runner.lock() {
Ok(runner) => runner,
Err(err) => crashln!("Unable to lock mutex: {err}"),
}
}};
}
fn kill_children(children: Vec<i64>) {
for pid in children {
if let Err(err) = kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
log::error!("Failed to stop pid {pid}: {err:?}");
};
}
}
impl Runner {
pub fn new() -> Self { dump::read() }
pub fn connect(name: String, Server { address, token }: Server, verbose: bool) -> Option<Self> {
let remote_config = match config::from(&address, token.as_deref()) {
Ok(config) => config,
Err(err) => {
log::error!("{err}");
return None;
}
};
if let Ok(dump) = dump::from(&address, token.as_deref()) {
then!(verbose, println!("{} Fetched remote (name={name}, address={address})", *helpers::SUCCESS));
Some(Runner {
remote: Some(Remote {
token,
address: string!(address),
config: remote_config,
}),
..dump
})
} else {
None
}
}
pub fn start(&mut self, name: &String, command: &String, path: PathBuf, watch: &Option<String>) -> &mut Self {
if let Some(remote) = &self.remote {
if let Err(err) = http::create(remote, name, command, path, watch) {
crashln!("{} Failed to start create {name}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
let id = self.id.next();
let config = config::read().runner;
let crash = Crash { crashed: false, value: 0 };
let watch = match watch {
Some(watch) => Watch {
enabled: true,
path: string!(watch),
hash: hash::create(file::cwd().join(watch)),
},
None => Watch {
enabled: false,
path: string!(""),
hash: string!(""),
},
};
let pid = run(ProcessMetadata {
args: config.args,
name: name.clone(),
shell: config.shell,
command: command.clone(),
log_path: config.log_path,
env: unix::env(),
});
self.list.insert(
id,
Process {
id,
pid,
path,
watch,
crash,
restarts: 0,
running: true,
children: vec![],
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();
kill_children(process.children.clone());
stop(process.pid);
if let Err(err) = std::env::set_current_dir(&path) {
crashln!("{} Failed to set working directory {:?}\nError: {:#?}", *helpers::FAIL, path, err);
};
process.pid = run(ProcessMetadata {
args: config.args,
name: name.clone(),
shell: config.shell,
log_path: config.log_path,
command: script.to_string(),
env: unix::env(),
});
- println!("{:?}", process.pid);
-
process.running = true;
process.children = vec![];
process.started = Utc::now();
process.crash.crashed = false;
process.env = env::vars().collect();
- println!("{:?}", process);
-
then!(dead, process.restarts += 1);
then!(dead, process.crash.value += 1);
then!(!dead, process.crash.value = 0);
}
return self;
}
pub fn remove(&mut self, id: usize) {
if let Some(remote) = &self.remote {
if let Err(err) = http::remove(remote, id) {
crashln!("{} Failed to stop remove {id}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
self.stop(id);
self.list.remove(&id);
dump::write(&self);
}
}
pub fn set_id(&mut self, id: id::Id) {
self.id = id;
self.id.next();
dump::write(&self);
}
pub fn set_status(&mut self, id: usize, status: Status) {
self.process(id).running = status.to_bool();
dump::write(&self);
}
pub fn items(&self) -> BTreeMap<usize, Process> { self.list.clone() }
+
pub fn items_mut(&mut self) -> &mut BTreeMap<usize, Process> { &mut self.list }
pub fn save(&self) { then!(self.remote.is_none(), dump::write(&self)) }
+
pub fn count(&mut self) -> usize { self.list().count() }
+
pub fn is_empty(&self) -> bool { self.list.is_empty() }
+
pub fn exists(&self, id: usize) -> bool { self.list.contains_key(&id) }
+
pub fn info(&self, id: usize) -> Option<&Process> { self.list.get(&id) }
+
pub fn list<'l>(&'l mut self) -> impl Iterator<Item = (&'l usize, &'l mut Process)> { self.list.iter_mut().map(|(k, v)| (k, v)) }
+
pub fn process(&mut self, id: usize) -> &mut Process { self.list.get_mut(&id).unwrap_or_else(|| crashln!("{} Process ({id}) not found", *helpers::FAIL)) }
+
pub fn pid(&self, id: usize) -> i64 { self.list.get(&id).unwrap_or_else(|| crashln!("{} Process ({id}) not found", *helpers::FAIL)).pid }
+ pub fn find(&self, name: &str) -> Option<usize> { self.list.iter().find(|(_, p)| p.name == name).map(|(id, _)| *id) }
+
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 set_children(&mut self, id: usize, children: Vec<i64>) -> &mut Self {
self.process(id).children = children;
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);
kill_children(process.children.clone());
stop(process.pid);
process.running = false;
process.crash.crashed = false;
process.crash.value = 0;
process.children = vec![];
}
return self;
}
pub fn rename(&mut self, id: usize, name: String) -> &mut Self {
if let Some(remote) = &self.remote {
if let Err(err) = http::rename(remote, id, name) {
crashln!("{} Failed to rename process {id}\nError: {:#?}", *helpers::FAIL, err);
};
} else {
self.process(id).name = name;
}
return self;
}
pub fn watch(&mut self, id: usize, path: &str, enabled: bool) -> &mut Self {
let process = self.process(id);
process.watch = Watch {
enabled,
path: string!(path),
hash: ternary!(enabled, hash::create(process.path.join(path)), string!("")),
};
return self;
}
pub fn fetch(&self) -> Vec<ProcessItem> {
let mut processes: Vec<ProcessItem> = Vec::new();
for (id, item) in self.items() {
let mut memory_usage: Option<MemoryInfo> = None;
let mut cpu_percent: Option<f32> = None;
if let Ok(mut process) = process::Process::new(item.pid as u32) {
let mem_info_psutil = process.memory_info().ok();
cpu_percent = process.cpu_percent().ok();
memory_usage = Some(MemoryInfo {
rss: mem_info_psutil.as_ref().unwrap().rss(),
vms: mem_info_psutil.as_ref().unwrap().vms(),
});
}
let cpu_percent = match cpu_percent {
Some(percent) => format!("{:.2}%", percent),
None => string!("0.00%"),
};
let memory_usage = match memory_usage {
Some(usage) => helpers::format_memory(usage.rss),
None => string!("0b"),
};
let status = if item.running {
string!("online")
} else {
match item.crash.crashed {
true => string!("crashed"),
false => string!("stopped"),
}
};
processes.push(ProcessItem {
id,
status,
pid: item.pid,
cpu: cpu_percent,
mem: memory_usage,
restarts: item.restarts,
name: item.name.clone(),
start_time: item.started,
watch_path: item.watch.path.clone(),
uptime: helpers::format_duration(item.started),
});
}
return processes;
}
}
impl Process {
/// Get a log paths of the process item
pub fn logs(&self) -> LogInfo {
let name = self.name.replace(" ", "_");
LogInfo {
out: global!("pmc.logs.out", name.as_str()),
error: global!("pmc.logs.error", name.as_str()),
}
}
}
impl ProcessWrapper {
/// Stop the process item
pub fn stop(&mut self) { lock!(self.runner).stop(self.id).save(); }
/// Restart the process item
pub fn restart(&mut self) { lock!(self.runner).restart(self.id, false).save(); }
/// Rename the process item
pub fn rename(&mut self, name: String) { lock!(self.runner).rename(self.id, name).save(); }
/// Enable watching a path on the process item
pub fn watch(&mut self, path: &str) { lock!(self.runner).watch(self.id, path, true).save(); }
/// Disable watching on the process item
pub fn disable_watch(&mut self) { lock!(self.runner).watch(self.id, "", false).save(); }
/// Set the process item as crashed
pub fn crashed(&mut self) { lock!(self.runner).restart(self.id, true).save(); }
/// Get a json dump of the process item
pub fn fetch(&self) -> ItemSingle {
let mut runner = lock!(self.runner);
let item = runner.process(self.id);
let config = config::read().runner;
let mut memory_usage: Option<MemoryInfo> = None;
let mut cpu_percent: Option<f32> = None;
if let Ok(mut process) = process::Process::new(item.pid as u32) {
let mem_info_psutil = process.memory_info().ok();
cpu_percent = process.cpu_percent().ok();
memory_usage = Some(MemoryInfo {
rss: mem_info_psutil.as_ref().unwrap().rss(),
vms: mem_info_psutil.as_ref().unwrap().vms(),
});
}
let status = if item.running {
string!("online")
} else {
match item.crash.crashed {
true => string!("crashed"),
false => string!("stopped"),
}
};
ItemSingle {
info: Info {
status,
id: item.id,
pid: item.pid,
name: item.name.clone(),
path: item.path.clone(),
children: item.children.clone(),
uptime: helpers::format_duration(item.started),
command: format!("{} {} '{}'", config.shell, config.args.join(" "), item.script.clone()),
},
stats: Stats {
cpu_percent,
memory_usage,
restarts: item.restarts,
start_time: item.started.timestamp_millis(),
},
watch: Watch {
enabled: item.watch.enabled,
hash: item.watch.hash.clone(),
path: item.watch.path.clone(),
},
log: Log {
out: item.logs().out,
error: item.logs().error,
},
raw: Raw {
running: item.running,
crashed: item.crash.crashed,
crashes: item.crash.value,
},
}
}
}
pub mod dump;
pub mod hash;
pub mod http;
pub mod id;
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Feb 1, 6:26 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
494881
Default Alt Text
(55 KB)
Attached To
Mode
rPMC Process Management Controller
Attached
Detach File
Event Timeline
Log In to Comment