use colored::Colorize; use macros_rs::{crashln, string, ternary, then}; use psutil::process::{MemoryInfo, Process}; use regex::Regex; use serde::Serialize; use serde_json::json; 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, }; pub struct Internal<'i> { pub id: usize, pub runner: Runner, pub kind: String, pub server_name: &'i str, } impl<'i> Internal<'i> { pub fn create(mut self, script: &String, name: &Option, watch: &Option) { 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.into(), 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(mut self, name: &Option, watch: &Option, silent: bool) -> Runner { then!(!silent, 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(); self.runner = item.get_runner().clone(); } 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.into(), 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) }; } if !silent { println!("{} Restarted {}({}) ✓", *helpers::SUCCESS, self.kind, self.id); log!("process started (id={})", self.id); } return self.runner; } pub fn stop(mut self, silent: bool) -> Runner { then!(!silent, 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.into(), 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) }; } let mut item = self.runner.get(self.id); item.stop(); self.runner = item.get_runner().clone(); if !silent { println!("{} Stopped {}({}) ✓", *helpers::SUCCESS, self.kind, self.id); log!("process stopped {}(id={})", self.kind, self.id); } return self.runner; } 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.into(), 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); } pub fn info(&self, format: &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(&self, serializer: S) -> Result { 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| { 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 ({})", self.kind, self.id).on_bright_white().black()); println!(" {}", format!("Use `pmc logs {} [--lines ]` to display logs", self.id).white()); println!(" {}", format!("Use `pmc env {}` to display environment variables", self.id).white()); } }; }; }; if matches!(self.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(self.id); let mut memory_usage: Option = None; let mut cpu_percent: Option = None; let path = file::make_relative(&item.path, &home).to_string_lossy().into_owned(); let children = if item.children.is_empty() { "none".to_string() } else { format!("{:?}", item.children) }; if let Ok(mut process) = Process::new(item.pid as u32) { 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!(self.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(self.server_name) { data = match Runner::connect(self.server_name.into(), server.get(), false) { Some(mut remote) => (remote.process(self.id).clone(), 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) }; let (item, remote) = data; let remote = remote.remote.unwrap(); let info = http::info(&remote, self.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::().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!(self.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(mut self, lines: &usize) { 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.into(), 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) }; let item = self.runner.info(self.id).unwrap_or_else(|| crashln!("{} Process ({}) not found", *helpers::FAIL, self.id)); println!( "{}", format!("Showing last {lines} lines for {}process [{}] (change the value with --lines option)", self.kind, self.id).yellow() ); for kind in vec!["error", "out"] { let logs = http::logs(&self.runner.remote.as_ref().unwrap(), self.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, self.id, kind, &item.name) } } } else { let item = self.runner.info(self.id).unwrap_or_else(|| crashln!("{} Process ({}) not found", *helpers::FAIL, self.id)); println!( "{}", format!("Showing last {lines} lines for {}process [{}] (change the value with --lines option)", self.kind, self.id).yellow() ); file::logs(item, *lines, "error"); file::logs(item, *lines, "out"); } } pub fn env(mut self) { println!("{}", format!("Showing env for {}process {}:\n", self.kind, self.id).bright_yellow()); 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.into(), 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) }; } let item = self.runner.process(self.id); item.env.iter().for_each(|(key, value)| println!("{}: {}", key, value.green())); } pub fn save(server_name: &String) { if !matches!(&**server_name, "internal" | "local") { crashln!("{} Cannot force save on remote servers", *helpers::FAIL) } println!("{} Saved current processes to dumpfile", *helpers::SUCCESS); Runner::new().save(); } pub fn restore(server_name: &String) { let mut runner = Runner::new(); let (kind, list_name) = super::format(server_name); if !matches!(&**server_name, "internal" | "local") { crashln!("{} Cannot restore on remote servers", *helpers::FAIL) } Runner::new().list().for_each(|(id, p)| { if p.running == true { runner = Internal { id: *id, server_name, kind: kind.clone(), runner: runner.clone(), } .restart(&None, &None, true); } }); println!("{} Restored process statuses from dumpfile", *helpers::SUCCESS); Internal::list(&string!("default"), &list_name); } pub fn list(format: &String, server_name: &String) { let render_list = |runner: &mut Runner, internal: bool| { let mut processes: Vec = 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(&self, serializer: S) -> Result { 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, Option) = (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::().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); } } }