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;