diff --git a/src/cli.rs b/src/cli.rs index 5c69d5f..b0ecd41 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,375 +1,426 @@ use colored::Colorize; use global_placeholders::global; use macros_rs::{crashln, string, ternary}; use psutil::process::{MemoryInfo, Process}; use serde::Serialize; use serde_json::json; use std::env; use pmc::{ config, file::{self, Exists}, helpers::{self, ColoredString}, log, process::Runner, }; use tabled::{ settings::{ object::{Columns, Rows}, style::{BorderColor, Style}, themes::Colorization, Color, Modify, Rotate, Width, }, Table, Tabled, }; #[derive(Clone, Debug)] pub enum Args { Id(usize), Script(String), } +fn format(server_name: &String) -> (String, String) { + let kind = ternary!(server_name == "internal", "", "remote ").to_string(); + let list_name = ternary!(*server_name == "internal", "all", &*server_name).to_string(); + + return (kind, list_name); +} + pub fn get_version(short: bool) -> String { return match short { true => format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), false => format!("{} ({} {}) [{}]", env!("CARGO_PKG_VERSION"), env!("GIT_HASH"), env!("BUILD_DATE"), env!("PROFILE")), }; } -pub fn start(name: &Option<String>, args: &Option<Args>, watch: &Option<String>) { +pub fn start(name: &Option<String>, args: &Option<Args>, watch: &Option<String>, server_name: &String) { let mut runner = Runner::new(); let config = config::read(); match args { Some(Args::Id(id)) => { - println!("{} Applying action restartProcess on ({id})", *helpers::SUCCESS); - let item = runner.get(*id); + let (kind, list_name) = format(server_name); + println!("{} Applying {kind}action restartProcess on ({id})", *helpers::SUCCESS); - match watch { - Some(path) => item.watch(path), - None => item.disable_watch(), - } + if *server_name == "internal" { + let item = runner.get(*id); - name.as_ref().map(|n| item.rename(n.trim().replace("\n", ""))); - item.restart(); + match watch { + Some(path) => item.watch(path), + None => item.disable_watch(), + } + + name.as_ref().map(|n| item.rename(n.trim().replace("\n", ""))); + item.restart(); - println!("{} restarted ({id}) ✓", *helpers::SUCCESS); - log!("process started (id={id})"); - list(&string!("default")); + log!("process started (id={id})"); + } else { + let Some(servers) = config::servers().servers else { + crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL) + }; + + if let Some(server) = servers.get(server_name) { + match Runner::connect(server_name.clone(), server.clone(), false) { + Some(mut runner) => runner.get(*id).restart(), + None => println!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address), + } + } + } + + println!("{} restarted {kind}({id}) ✓", *helpers::SUCCESS); + list(&string!("default"), &list_name); } Some(Args::Script(script)) => { let name = match name { Some(name) => string!(name), None => string!(script.split_whitespace().next().unwrap_or_default()), }; // fix println!("{} Creating process with ({name})", *helpers::SUCCESS); if name.ends_with(".ts") || name.ends_with(".js") { let script = format!("{} {script}", config.runner.node); runner.start(&name, &script, watch).save(); } else { runner.start(&name, script, watch).save(); } println!("{} created ({name}) ✓", *helpers::SUCCESS); log!("process created (name={name})"); - list(&string!("default")); + list(&string!("default"), &string!("all")); } None => {} } } -pub fn stop(id: &usize) { - println!("{} Applying action stopProcess on ({id})", *helpers::SUCCESS); - let mut runner = Runner::new(); - runner.get(*id).stop(); +pub fn stop(id: &usize, server_name: &String) { + let (kind, list_name) = format(server_name); + println!("{} Applying {kind}action stopProcess on ({id})", *helpers::SUCCESS); + + if *server_name == "internal" { + let mut runner = Runner::new(); + runner.get(*id).stop(); + log!("process stopped (id={id})"); + } else { + let Some(servers) = config::servers().servers else { + crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL) + }; + + if let Some(server) = servers.get(server_name) { + match Runner::connect(server_name.clone(), server.clone(), false) { + Some(mut runner) => runner.get(*id).stop(), + None => println!("{} Failed to connect (name={server_name}, address={})", *helpers::FAIL, server.address), + } + } + } - println!("{} stopped ({id}) ✓", *helpers::SUCCESS); - log!("process stopped (id={id})"); - list(&string!("default")); + println!("{} stopped {kind}({id}) ✓", *helpers::SUCCESS); + list(&string!("default"), &list_name); } pub fn remove(id: &usize) { println!("{} Applying action removeProcess on ({id})", *helpers::SUCCESS); Runner::new().remove(*id); println!("{} removed ({id}) ✓", *helpers::SUCCESS); log!("process removed (id={id})"); } pub fn info(id: &usize, 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, #[tabled(rename = "exec cwd")] path: String, #[tabled(rename = "script command ")] command: String, #[tabled(rename = "script id")] id: String, restarts: u64, uptime: String, pid: String, name: String, status: ColoredString, } impl Serialize for Info { fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { let trimmed_json = json!({ "id": &self.id.trim(), "pid": &self.pid.trim(), "name": &self.name.trim(), "path": &self.path.trim(), "restarts": &self.restarts, "watch": &self.watch.trim(), "watch": &self.hash.trim(), "uptime": &self.uptime.trim(), "status": &self.status.0.trim(), "log_out": &self.log_out.trim(), "cpu": &self.cpu_percent.trim(), "command": &self.command.trim(), "mem": &self.memory_usage.trim(), "log_error": &self.log_error.trim(), }); trimmed_json.serialize(serializer) } } if let Some(home) = home::home_dir() { let config = config::read().runner; let item = Runner::new().get(*id).clone(); let mut memory_usage: Option<MemoryInfo> = None; let mut cpu_percent: Option<f32> = None; 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 path = file::make_relative(&item.path, &home) .map(|relative_path| relative_path.to_string_lossy().into_owned()) .unwrap_or_else(|| crashln!("{} Unable to get your current directory", *helpers::FAIL)); let data = vec![Info { cpu_percent, memory_usage, id: string!(id), restarts: item.restarts, name: item.name.clone(), path: format!("{} ", path), status: ColoredString(status), log_out: global!("pmc.logs.out", item.name.as_str()), log_error: global!("pmc.logs.error", item.name.as_str()), pid: ternary!(item.running, format!("{}", item.pid), string!("n/a")), command: format!("{} {} '{}'", config.shell, config.args.join(" "), item.script), hash: ternary!(item.watch.enabled, format!("{} ", item.watch.hash), string!("none ")), watch: ternary!(item.watch.enabled, format!("{path}/{} ", item.watch.path), string!("disabled ")), uptime: ternary!(item.running, format!("{}", helpers::format_duration(item.started)), string!("none")), }]; 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()); } }; }; } else { crashln!("{} Impossible to get your home directory", *helpers::FAIL); } } pub fn logs(id: &usize, lines: &usize) { let item = Runner::new().get(*id).clone(); let log_error = global!("pmc.logs.error", item.name.as_str()); let log_out = global!("pmc.logs.out", item.name.as_str()); if Exists::file(log_error.clone()).unwrap() && Exists::file(log_out.clone()).unwrap() { println!("{}", format!("Showing last {lines} lines for process [{id}] (change the value with --lines option)").yellow()); file::logs(*lines, &log_error, *id, "error", &item.name); file::logs(*lines, &log_out, *id, "out", &item.name); } else { crashln!("{} Logs for process ({id}) not found", *helpers::FAIL); } } pub fn env(id: &usize) { let item = Runner::new().get(*id).clone(); for (key, value) in item.env.iter() { println!("{}: {}", key, value.green()); } } -pub fn list(format: &String) { +pub fn list(format: &String, server_name: &String) { let render_list = |runner: &mut Runner| { 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 memory_usage: Option<MemoryInfo> = None; let mut cpu_percent: Option<f32> = None; 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!("{:.0}%", 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() }; processes.push(ProcessItem { status: ColoredString(status), cpu: format!("{cpu_percent} "), mem: format!("{memory_usage} "), restarts: format!("{} ", item.restarts), name: format!("{} ", item.name.clone()), id: ColoredString(id.to_string().cyan().bold()), pid: ternary!(item.running, format!("{} ", item.pid), string!("n/a ")), watch: ternary!(item.watch.enabled, format!("{} ", item.watch.path), string!("disabled ")), uptime: ternary!(item.running, format!("{} ", helpers::format_duration(item.started)), string!("none ")), }); } let table = Table::new(&processes) .with(Style::rounded().remove_verticals()) .with(BorderColor::filled(Color::FG_BRIGHT_BLACK)) .with(Colorization::exact([Color::FG_BRIGHT_CYAN], Rows::first())) .with(Modify::new(Columns::single(1)).with(Width::truncate(35).suffix("... "))) .to_string(); if let Ok(json) = serde_json::to_string(&processes) { match format.as_str() { "raw" => println!("{:?}", processes), "json" => println!("{json}"), "default" => println!("{table}"), _ => {} }; }; } }; if let Some(servers) = config::servers().servers { let mut failed: Vec<(String, String)> = vec![]; - println!("{} Internal daemon", *helpers::SUCCESS); - render_list(&mut Runner::new()); - - for (name, server) in servers { - match Runner::connect(name.clone(), server.clone()) { + if let Some(server) = servers.get(server_name) { + match Runner::connect(server_name.clone(), server.clone(), true) { Some(mut runner) => render_list(&mut runner), - None => failed.push((name.clone(), server.address.clone())), + None => println!("{} Failed to fetch (name={server_name}, address={})", *helpers::FAIL, server.address), + } + } else { + if matches!(&**server_name, "internal" | "all") { + println!("{} Internal daemon", *helpers::SUCCESS); + render_list(&mut Runner::new()); + } else { + crashln!("{} Server '{server_name}' does not exist", *helpers::FAIL); + } + } + + if *server_name == "all" { + for (name, server) in servers { + match Runner::connect(name.clone(), server.clone(), true) { + Some(mut runner) => render_list(&mut runner), + 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()); } } diff --git a/src/main.rs b/src/main.rs index b76881f..ebe1f69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,142 +1,169 @@ mod cli; mod daemon; mod globals; mod webui; use crate::cli::Args; use clap::{Parser, Subcommand}; use clap_verbosity_flag::Verbosity; use macros_rs::{str, string, then}; fn validate_id_script(s: &str) -> Result<Args, String> { if let Ok(id) = s.parse::<usize>() { Ok(Args::Id(id)) } else { Ok(Args::Script(s.to_owned())) } } #[derive(Parser)] #[command(version = str!(cli::get_version(false)))] struct Cli { #[command(subcommand)] command: Commands, #[clap(flatten)] verbose: Verbosity, } #[derive(Subcommand)] enum Daemon { /// Reset process index #[command(alias = "clean")] Reset, /// Stop daemon #[command(alias = "kill")] Stop, /// Restart daemon #[command(alias = "restart", alias = "start")] Restore { /// Daemon api #[arg(long)] api: bool, /// WebUI using api #[arg(long)] webui: bool, }, /// Check daemon #[command(alias = "info", alias = "status")] Health { /// Format output #[arg(long, default_value_t = string!("default"))] format: String, }, } // add pmc restore command #[derive(Subcommand)] enum Commands { /// Start/Restart a process #[command(alias = "restart")] Start { /// Process name #[arg(long)] name: Option<String>, #[clap(value_parser = validate_id_script)] args: Option<Args>, /// Watch to reload path #[arg(long)] watch: Option<String>, + /// Server + #[arg(short, long, default_value_t = string!("internal"))] + server: String, }, /// Stop/Kill a process #[command(alias = "kill")] - Stop { id: usize }, + Stop { + id: usize, + /// Server + #[arg(short, long, default_value_t = string!("internal"))] + server: String, + }, /// Stop then remove a process #[command(alias = "rm")] - Remove { id: usize }, + Remove { + id: usize, + /// Server + #[arg(short, long, default_value_t = string!("internal"))] + server: String, + }, /// Get env of a process #[command(alias = "cmdline")] - Env { id: usize }, + Env { + id: usize, + /// Server + #[arg(short, long, default_value_t = string!("internal"))] + server: String, + }, /// Get information of a process #[command(alias = "info")] Details { id: usize, /// Format output #[arg(long, default_value_t = string!("default"))] format: String, + /// Server + #[arg(short, long, default_value_t = string!("internal"))] + server: String, }, /// List all processes #[command(alias = "ls")] List { /// Format output #[arg(long, default_value_t = string!("default"))] format: String, + /// Server + #[arg(short, long, default_value_t = string!("all"))] + server: String, }, /// Get logs from a process Logs { id: usize, #[arg(long, default_value_t = 15, help = "")] lines: usize, + /// Server + #[arg(short, long, default_value_t = string!("internal"))] + server: String, }, /// Daemon management Daemon { #[command(subcommand)] command: Daemon, }, } fn main() { let cli = Cli::parse(); let mut env = env_logger::Builder::new(); let level = cli.verbose.log_level_filter(); globals::init(); env.filter_level(level).init(); match &cli.command { - Commands::Start { name, args, watch } => cli::start(name, args, watch), - Commands::Stop { id } => cli::stop(id), - Commands::Remove { id } => cli::remove(id), - Commands::Env { id } => cli::env(id), - Commands::Details { id, format } => cli::info(id, format), - Commands::List { format } => cli::list(format), - Commands::Logs { id, lines } => cli::logs(id, lines), + Commands::Start { name, args, watch, server } => cli::start(name, args, watch, server), + Commands::Stop { id, server } => cli::stop(id, server), + Commands::Remove { id, server } => cli::remove(id), + Commands::Env { id, server } => cli::env(id), + Commands::Details { id, format, server } => cli::info(id, format), + Commands::List { format, server } => cli::list(format, server), + Commands::Logs { id, lines, server } => cli::logs(id, lines), Commands::Daemon { command } => match command { Daemon::Stop => daemon::stop(), Daemon::Reset => daemon::reset(), Daemon::Health { format } => daemon::health(format), Daemon::Restore { api, webui } => daemon::restart(api, webui, level.as_str() != "ERROR"), }, }; if !matches!(&cli.command, Commands::Daemon { .. }) { then!(!daemon::pid::exists(), daemon::start(false)); } } diff --git a/src/process/http.rs b/src/process/http.rs index d86a56e..d47ab91 100644 --- a/src/process/http.rs +++ b/src/process/http.rs @@ -1,18 +1,43 @@ use crate::process::{Process, Remote}; -use macros_rs::fmtstr; -use reqwest::blocking::Client; +use macros_rs::{fmtstr, string}; +use reqwest::blocking::{Client, Response}; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use serde::Serialize; use std::collections::BTreeMap; -pub fn list(remote: &Remote) -> Result<BTreeMap<usize, Process>, anyhow::Error> { +#[derive(Serialize)] +struct ActionBody { + pub method: String, +} + +fn client(token: &Option<String>) -> (Client, HeaderMap) { let client = Client::new(); let mut headers = HeaderMap::new(); - let Remote { address, token } = remote; if let Some(token) = token { headers.insert(AUTHORIZATION, HeaderValue::from_static(fmtstr!("token {token}"))); } - let response = client.get(fmtstr!("{address}/list")).headers(headers).send()?; - Ok(response.json()?) + return (client, headers); +} + +pub fn restart(Remote { address, token }: &Remote, id: usize) -> Result<Response, anyhow::Error> { + let (client, headers) = client(token); + let content = ActionBody { method: string!("restart") }; + + Ok(client.post(fmtstr!("{address}/process/{id}/action")).json(&content).headers(headers).send()?) +} + +pub fn stop(Remote { address, token }: &Remote, id: usize) -> Result<Response, anyhow::Error> { + let (client, headers) = client(token); + let content = ActionBody { method: string!("stop") }; + + Ok(client.post(fmtstr!("{address}/process/{id}/action")).json(&content).headers(headers).send()?) +} + +pub fn remove(Remote { address, token }: &Remote, id: usize) -> Result<Response, anyhow::Error> { + let (client, headers) = client(token); + let content = ActionBody { method: string!("remove") }; + + Ok(client.post(fmtstr!("{address}/process/{id}/action")).json(&content).headers(headers).send()?) } diff --git a/src/process/mod.rs b/src/process/mod.rs index a23f0f6..7ef1d6a 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,418 +1,434 @@ mod http; use crate::{ config, config::structs::Server, file, helpers, service::{run, stop, ProcessMetadata}, }; use chrono::serde::ts_milliseconds; use chrono::{DateTime, Utc}; use global_placeholders::global; use macros_rs::{crashln, string, ternary, then}; use psutil::process::{self, MemoryInfo}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::{BTreeMap, HashMap}; use std::{env, path::PathBuf}; use utoipa::ToSchema; #[derive(Serialize, ToSchema)] pub struct ItemSingle { info: Info, stats: Stats, watch: Watch, log: Log, raw: Raw, } #[derive(Serialize, ToSchema)] pub struct Info { id: usize, pid: i64, name: String, status: String, #[schema(value_type = String, example = "/path")] path: PathBuf, uptime: String, command: String, } #[derive(Serialize, ToSchema)] pub struct Stats { restarts: u64, start_time: i64, cpu_percent: Option<f32>, memory_usage: Option<MemoryInfo>, } #[derive(Serialize, ToSchema)] pub struct Log { out: String, error: String, } #[derive(Serialize, ToSchema)] pub struct Raw { running: bool, crashed: bool, crashes: u64, } #[derive(Serialize, ToSchema)] pub struct ProcessItem { pid: i64, id: usize, cpu: String, mem: String, name: String, restarts: u64, status: String, uptime: String, #[schema(example = "/path")] watch_path: String, #[schema(value_type = String, example = "2000-01-01T01:00:00.000Z")] start_time: DateTime<Utc>, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Process { pub id: usize, pub pid: i64, pub name: String, pub path: PathBuf, pub script: String, pub env: HashMap<String, String>, #[serde(with = "ts_milliseconds")] pub started: DateTime<Utc>, pub restarts: u64, pub running: bool, pub crash: Crash, pub watch: Watch, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Crash { pub crashed: bool, pub value: u64, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct Watch { pub enabled: bool, #[schema(example = "/path")] pub path: String, pub hash: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Runner { pub id: id::Id, #[serde(skip)] pub remote: Option<Remote>, pub list: BTreeMap<usize, Process>, } #[derive(Clone, Debug)] pub struct Remote { address: String, token: Option<String>, } pub enum Status { Offline, Running, } impl Status { pub fn to_bool(&self) -> bool { match self { Status::Offline => false, Status::Running => true, } } } impl Runner { pub fn new() -> Self { dump::read() } - pub fn connect(name: String, server: Server) -> Option<Self> { - let Server { address, token } = server; - + pub fn connect(name: String, Server { address, token }: Server, verbose: bool) -> Option<Self> { match dump::from(&address, token.as_deref()) { Ok(dump) => { - println!("{} Fetched remote (name={name}, address={address})", *helpers::SUCCESS); + then!(verbose, println!("{} Fetched remote (name={name}, address={address})", *helpers::SUCCESS)); return Some(Runner { remote: Some(Remote { token, address: string!(address) }), ..dump }); } Err(err) => { log::debug!("{err}"); return None; } } } pub fn start(&mut self, name: &String, command: &String, watch: &Option<String>) -> &mut Self { let id = self.id.next(); let config = config::read().runner; let crash = Crash { crashed: false, value: 0 }; let watch = match watch { Some(watch) => Watch { enabled: true, path: string!(watch), hash: hash::create(file::cwd().join(watch)), }, None => { Watch { enabled: false, path: string!(""), hash: string!(""), } } }; let pid = run(ProcessMetadata { args: config.args, name: name.clone(), shell: config.shell, command: command.clone(), log_path: config.log_path, }); self.list.insert( id, Process { id, pid, watch, crash, restarts: 0, running: true, path: file::cwd(), 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 { - let item = self.get(id); - let Process { path, script, name, .. } = item.clone(); + 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 item = self.get(id); + let Process { path, script, name, .. } = item.clone(); - if let Err(err) = std::env::set_current_dir(&item.path) { - crashln!("{} Failed to set working directory {:?}\nError: {:#?}", *helpers::FAIL, path, err); - }; + if let Err(err) = std::env::set_current_dir(&item.path) { + crashln!("{} Failed to set working directory {:?}\nError: {:#?}", *helpers::FAIL, path, err); + }; - item.stop(); + item.stop(); - let config = config::read().runner; + let config = config::read().runner; - item.crash.crashed = false; - item.pid = run(ProcessMetadata { - command: script, - args: config.args, - name: name.clone(), - shell: config.shell, - log_path: config.log_path, - }); + item.crash.crashed = false; + item.pid = run(ProcessMetadata { + command: script, + args: config.args, + name: name.clone(), + shell: config.shell, + log_path: config.log_path, + }); - item.running = true; - item.started = Utc::now(); - then!(dead, item.restarts += 1); + item.running = true; + item.started = Utc::now(); + then!(dead, item.restarts += 1); + } return self; } pub fn remove(&mut self, id: usize) { - self.stop(id); - self.list.remove(&id); - dump::write(&self); + 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.get(id).running = status.to_bool(); dump::write(&self); } pub fn items(&mut self) -> BTreeMap<usize, Process> { self.list.clone() } pub fn items_mut(&mut self) -> &mut BTreeMap<usize, Process> { &mut self.list } pub fn save(&self) { then!(self.remote.is_none(), dump::write(&self)) } pub fn count(&mut self) -> usize { self.list().count() } pub fn is_empty(&self) -> bool { self.list.is_empty() } pub fn exists(&mut self, id: usize) -> bool { self.list.contains_key(&id) } pub fn info(&mut self, id: usize) -> Option<&Process> { self.list.get(&id) } pub fn list<'l>(&'l mut self) -> impl Iterator<Item = (&'l usize, &'l mut Process)> { self.list.iter_mut().map(|(k, v)| (k, v)) } pub fn get(&mut self, id: usize) -> &mut Process { self.list.get_mut(&id).unwrap_or_else(|| crashln!("{} Process ({id}) not found", *helpers::FAIL)) } pub fn set_crashed(&mut self, id: usize) -> &mut Self { self.get(id).crash.crashed = true; return self; } pub fn new_crash(&mut self, id: usize) -> &mut Self { self.get(id).crash.value += 1; return self; } pub fn stop(&mut self, id: usize) -> &mut Self { - let item = self.get(id); - stop(item.pid); + 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 item = self.get(id); + stop(item.pid); - item.running = false; - item.crash.crashed = false; - item.crash.value = 0; + item.running = false; + item.crash.crashed = false; + item.crash.value = 0; + } return self; } pub fn rename(&mut self, id: usize, name: String) -> &mut Self { self.get(id).name = name; return self; } pub fn watch(&mut self, id: usize, path: &str, enabled: bool) -> &mut Self { let item = self.get(id); item.watch = Watch { enabled, path: string!(path), hash: ternary!(enabled, hash::create(item.path.join(path)), string!("")), }; return self; } pub fn json(&mut self) -> Value { let mut processes: Vec<ProcessItem> = Vec::new(); for (id, item) in self.items() { let mut memory_usage: Option<MemoryInfo> = None; let mut cpu_percent: Option<f32> = None; if let Ok(mut process) = process::Process::new(item.pid as u32) { 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.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), }); } json!(processes) } } impl Process { pub fn stop(&mut self) { Runner::new().stop(self.id).save(); } pub fn watch(&mut self, path: &str) { Runner::new().watch(self.id, path, true).save(); } pub fn disable_watch(&mut self) { Runner::new().watch(self.id, "", false).save(); } pub fn rename(&mut self, name: String) { Runner::new().rename(self.id, name).save(); } pub fn restart(&mut self) { Runner::new().restart(self.id, false).save(); } pub fn crashed(&mut self) -> &mut Process { Runner::new().new_crash(self.id).save(); Runner::new().restart(self.id, true).save(); return self; } pub fn json(&mut self) -> Value { 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(self.pid as u32) { memory_usage = process.memory_info().ok(); cpu_percent = process.cpu_percent().ok(); } let status = if self.running { string!("online") } else { match self.crash.crashed { true => string!("crashed"), false => string!("stopped"), } }; json!(ItemSingle { info: Info { status, id: self.id, pid: self.pid, name: self.name.clone(), path: self.path.clone(), uptime: helpers::format_duration(self.started), command: format!("{} {} '{}'", config.shell, config.args.join(" "), self.script.clone()), }, stats: Stats { cpu_percent, memory_usage, restarts: self.restarts, start_time: self.started.timestamp_millis(), }, watch: Watch { enabled: self.watch.enabled, hash: self.watch.hash.clone(), path: self.watch.path.clone(), }, log: Log { out: global!("pmc.logs.out", self.name.as_str()), error: global!("pmc.logs.error", self.name.as_str()), }, raw: Raw { running: self.running, crashed: self.crash.crashed, crashes: self.crash.value, } }) } } pub mod dump; pub mod hash; pub mod id;