diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d042e3a..b30e8fa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,49 +1,49 @@ stages: [build, release] -image: 'themackabu/rust:zigbuild-1.77.0' +image: 'themackabu/rust:zigbuild-1.79.0' before_script: - mkdir binary - apt-get update -yqq - apt-get install -yqq zip clang llvm - export CC="/usr/bin/clang" - export CXX="/usr/bin/clang++" build_linux_amd64: stage: build tags: [rust] only: [/\d+\.\d+\.\d+.*$/] script: - cargo zigbuild -r --color always - zip binary/pmc_${CI_COMMIT_TAG}_linux_amd64.zip target/release/pmc -j artifacts: paths: [binary/] build_linux_aarch64: stage: build tags: [rust] only: [/\d+\.\d+\.\d+.*$/] script: - cargo zigbuild -r --target aarch64-unknown-linux-gnu --color always - zip binary/pmc_${CI_COMMIT_TAG}_linux_aarch64.zip target/aarch64-unknown-linux-gnu/release/pmc -j artifacts: paths: [binary/] build_darwin_amd64: stage: build tags: [rust] only: [/\d+\.\d+\.\d+.*$/] script: - cargo zigbuild -r --target x86_64-apple-darwin --color always - zip binary/pmc_${CI_COMMIT_TAG}_darwin_amd64.zip target/x86_64-apple-darwin/release/pmc -j artifacts: paths: [binary/] build_darwin_aarch64: stage: build tags: [rust] only: [/\d+\.\d+\.\d+.*$/] script: - cargo zigbuild -r --target aarch64-apple-darwin --color always - zip binary/pmc_${CI_COMMIT_TAG}_darwin_arm.zip target/aarch64-apple-darwin/release/pmc -j artifacts: paths: [binary/] diff --git a/.harness-ci.yml b/.harness-ci.yml index d0f6820..ce1d857 100644 --- a/.harness-ci.yml +++ b/.harness-ci.yml @@ -1,33 +1,33 @@ version: 1 kind: pipeline spec: stages: - name: binary_x64 type: ci spec: steps: - name: build type: run spec: - container: themackabu/rust:zigbuild-1.77.0 + container: themackabu/rust:zigbuild-1.79.0 script: |- apt-get update -yqq apt-get install -yqq zip clang llvm export CC="/usr/bin/clang" export CXX="/usr/bin/clang++" cargo zigbuild -r -j 4 zip pmc_${{ build.commit }}-B${{ build.number }}.zip target/release/pmc -j - spec: inputs: access_key: ${{ secrets.get("pmc_s3_key") }} acl: read-write bucket: themackabu-bun-cdn region: us1 path_style: false endpoint: https://gateway.storjshare.io secret_key: ${{ secrets.get("pmc_s3_secret") }} target: gitness source: pmc_${{ build.commit }}-B${{ build.number }}.zip name: s3 type: plugin name: upload diff --git a/build.rs b/build.rs index 6c6ad36..cbe98d7 100644 --- a/build.rs +++ b/build.rs @@ -1,169 +1,168 @@ use chrono::Datelike; use flate2::read::GzDecoder; use reqwest; use tar::Archive; use std::{ env, fs::{self, File}, io::{self, copy}, path::{Path, PathBuf}, process::Command, }; const NODE_VERSION: &str = "20.11.0"; fn extract_tar_gz(tar: &PathBuf, download_dir: &PathBuf) -> io::Result<()> { let file = File::open(tar)?; let decoder = GzDecoder::new(file); let mut archive = Archive::new(decoder); archive.unpack(download_dir)?; Ok(fs::remove_file(tar)?) } fn download_file(url: String, destination: &PathBuf, download_dir: &PathBuf) { if !download_dir.exists() { fs::create_dir_all(download_dir).unwrap(); } let mut response = reqwest::blocking::get(url).expect("Failed to send request"); let mut file = File::create(destination).expect("Failed to create file"); copy(&mut response, &mut file).expect("Failed to copy content"); } fn download_node() -> PathBuf { #[cfg(target_os = "linux")] let target_os = "linux"; #[cfg(all(target_os = "macos"))] let target_os = "darwin"; #[cfg(all(target_arch = "arm"))] let target_arch = "armv7l"; #[cfg(all(target_arch = "x86_64"))] let target_arch = "x64"; #[cfg(all(target_arch = "aarch64"))] let target_arch = "arm64"; let download_url = format!("https://nodejs.org/dist/v{NODE_VERSION}/node-v{NODE_VERSION}-{target_os}-{target_arch}.tar.gz"); /* paths */ let download_dir = Path::new("target").join("downloads"); let node_extract_dir = download_dir.join(format!("node-v{NODE_VERSION}-{target_os}-{target_arch}")); if node_extract_dir.is_dir() { return node_extract_dir; } /* download node */ let node_archive = download_dir.join(format!("node-v{}-{}.tar.gz", NODE_VERSION, target_os)); download_file(download_url, &node_archive, &download_dir); /* extract node */ if let Err(err) = extract_tar_gz(&node_archive, &download_dir) { panic!("Failed to extract Node.js: {:?}", err) } println!("cargo:rustc-env=NODE_HOME={}", node_extract_dir.to_str().unwrap()); return node_extract_dir; } fn download_then_build(node_extract_dir: PathBuf) { let base_dir = match fs::canonicalize(node_extract_dir) { Ok(path) => path, Err(err) => panic!("{err}"), }; let bin = &base_dir.join("bin"); let node = &bin.join("node"); let project_dir = &Path::new("src").join("webui"); let npm = &base_dir.join("lib/node_modules/npm/index.js"); /* set path */ let mut paths = match env::var_os("PATH") { Some(paths) => env::split_paths(&paths).collect::>(), None => vec![], }; paths.push(bin.clone()); let path = match env::join_paths(paths) { Ok(joined) => joined, Err(err) => panic!("{err}"), }; /* install deps */ Command::new(node) .args([npm.to_str().unwrap(), "ci"]) .current_dir(project_dir) .env("PATH", &path) .status() .expect("Failed to install dependencies"); /* build frontend */ Command::new(node) .args(["node_modules/astro/astro.js", "build"]) .current_dir(project_dir) .env("PATH", &path) .status() .expect("Failed to build frontend"); } fn main() { #[cfg(target_os = "windows")] compile_error!("This project is not supported on Windows."); #[cfg(target_arch = "x86")] compile_error!("This project is not supported on 32 bit."); /* version attributes */ let date = chrono::Utc::now(); let profile = env::var("PROFILE").unwrap(); let output = Command::new("git").args(&["rev-parse", "--short=10", "HEAD"]).output().unwrap(); let output_full = Command::new("git").args(&["rev-parse", "HEAD"]).output().unwrap(); println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap()); println!("cargo:rustc-env=GIT_HASH={}", String::from_utf8(output.stdout).unwrap()); println!("cargo:rustc-env=GIT_HASH_FULL={}", String::from_utf8(output_full.stdout).unwrap()); println!("cargo:rustc-env=BUILD_DATE={}-{}-{}", date.year(), date.month(), date.day()); /* profile matching */ match profile.as_str() { "debug" => println!("cargo:rustc-env=PROFILE=debug"), - "release" => { - /* cleanup */ - fs::remove_dir_all(format!("src/webui/dist")).ok(); - println!("cargo:rustc-env=PROFILE=release"); - - /* pre-build */ - let path = download_node(); - download_then_build(path); - - /* cc linking */ - cxx_build::bridge("src/lib.rs") - .file("lib/bridge.cc") - .file("lib/process.cc") - .file("lib/fork.cc") - .include("lib/include") - .flag_if_supported("-std=c++17") - .compile("bridge"); - } + "release" => println!("cargo:rustc-env=PROFILE=release"), _ => println!("cargo:rustc-env=PROFILE=none"), } + /* cleanup */ + fs::remove_dir_all(format!("src/webui/dist")).ok(); + + /* pre-build */ + let path = download_node(); + download_then_build(path); + + /* cc linking */ + cxx_build::bridge("src/lib.rs") + .file("lib/bridge.cc") + .file("lib/process.cc") + .file("lib/fork.cc") + .include("lib/include") + .flag_if_supported("-std=c++17") + .compile("bridge"); + let watched = vec![ "lib", "src/lib.rs", "lib/include", "src/webui/src", "src/webui/links.ts", "src/webui/package.json", "src/webui/tsconfig.json", "src/webui/astro.config.mjs", "src/webui/tailwind.config.mjs", ]; watched.iter().for_each(|file| println!("cargo:rerun-if-changed={file}")); } diff --git a/src/cli/args.rs b/src/cli/args.rs new file mode 100644 index 0000000..a5a04fa --- /dev/null +++ b/src/cli/args.rs @@ -0,0 +1,34 @@ +pub trait Validatable { + fn from_id(id: usize) -> Self; + fn from_string(s: String) -> Self; +} + +#[derive(Clone)] +pub enum Args { + Id(usize), + Script(String), +} + +#[derive(Clone)] +pub enum Item { + Id(usize), + Name(String), +} + +impl Validatable for Args { + fn from_id(id: usize) -> Self { Args::Id(id) } + fn from_string(s: String) -> Self { Args::Script(s) } +} + +impl Validatable for Item { + fn from_id(id: usize) -> Self { Item::Id(id) } + fn from_string(s: String) -> Self { Item::Name(s) } +} + +pub fn validate(s: &str) -> Result { + if let Ok(id) = s.parse::() { + Ok(T::from_id(id)) + } else { + Ok(T::from_string(s.to_owned())) + } +} diff --git a/src/cli/internal.rs b/src/cli/internal.rs index 1fc9b74..681dc93 100644 --- a/src/cli/internal.rs +++ b/src/cli/internal.rs @@ -1,131 +1,578 @@ -use macros_rs::{crashln, string}; -use pmc::{config, file, helpers, log, process::Runner}; +use colored::Colorize; +use macros_rs::{crashln, string, ternary}; +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 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") { + 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) { + 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(self, name: &Option, watch: &Option) { println!("{} Applying {}action restartProcess on ({})", *helpers::SUCCESS, self.kind, self.id); - if matches!(&**self.server_name, "internal" | "local") { + 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) { + 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) }; } 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") { + 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) { + 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) }; } 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") { + 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) { + 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 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); + } + } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fa6b59e..ca89bdb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,520 +1,107 @@ +mod args; +pub use args::*; + 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 serde::Serialize; -use serde_json::json; +use pmc::{helpers, process::Runner}; 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, args: &Args, watch: &Option, server_name: &String) { let runner = Runner::new(); let (kind, list_name) = format(server_name); match args { Args::Id(id) => Internal { id: *id, runner, server_name, kind }.restart(name, watch), - Args::Script(script) => match runner.find(&script) { + Args::Script(script) => match runner.find(&script, server_name) { 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); + Internal::list(&string!("default"), &list_name); } pub fn stop(item: &Item, server_name: &String) { let runner: Runner = Runner::new(); let (kind, list_name) = format(server_name); match item { Item::Id(id) => Internal { id: *id, runner, server_name, kind }.stop(), - Item::Name(name) => match runner.find(&name) { + Item::Name(name) => match runner.find(&name, server_name) { Some(id) => Internal { id, runner, server_name, kind }.stop(), None => crashln!("{} Process ({name}) not found", *helpers::FAIL), }, } - list(&string!("default"), &list_name); + Internal::list(&string!("default"), &list_name); } pub fn remove(item: &Item, server_name: &String) { let runner: Runner = Runner::new(); let (kind, _) = format(server_name); match item { Item::Id(id) => Internal { id: *id, runner, server_name, kind }.remove(), - Item::Name(name) => match runner.find(&name) { + Item::Name(name) => match runner.find(&name, server_name) { Some(id) => Internal { id, runner, server_name, kind }.remove(), None => crashln!("{} Process ({name}) not found", *helpers::FAIL), }, } } -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(&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 ({id})").on_bright_white().black()); - println!(" {}", format!("Use `pmc logs {id} [--lines ]` 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 = 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!(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::().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()); +pub fn info(item: &Item, format: &String, server_name: &String) { + let runner: Runner = Runner::new(); + let (kind, _) = self::format(server_name); - file::logs(item, *lines, "error"); - file::logs(item, *lines, "out"); + match item { + Item::Id(id) => Internal { id: *id, runner, server_name, kind }.info(format), + Item::Name(name) => match runner.find(&name, server_name) { + Some(id) => Internal { id, runner, server_name, kind }.info(format), + None => crashln!("{} Process ({name}) not found", *helpers::FAIL), + }, } } -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) - }; +pub fn logs(item: &Item, lines: &usize, server_name: &String) { + let runner: Runner = Runner::new(); + let (kind, _) = format(server_name); - 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 }.logs(lines), + Item::Name(name) => match runner.find(&name, server_name) { + Some(id) => Internal { id, runner, server_name, kind }.logs(lines), + None => crashln!("{} Process ({name}) not found", *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 = 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)), - } - } - } +pub fn env(item: &Item, server_name: &String) { + let runner: Runner = Runner::new(); + let (kind, _) = format(server_name); - 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); + match item { + Item::Id(id) => Internal { id: *id, runner, server_name, kind }.env(), + Item::Name(name) => match runner.find(&name, server_name) { + Some(id) => Internal { id, runner, server_name, kind }.env(), + None => crashln!("{} Process ({name}) not found", *helpers::FAIL), + }, } } diff --git a/src/cli/server.rs b/src/cli/server.rs index 53403da..5d4ea58 100644 --- a/src/cli/server.rs +++ b/src/cli/server.rs @@ -1,143 +1,143 @@ use colored::Colorize; use inquire::{Confirm, Password, PasswordDisplayMode, Select, Text}; use macros_rs::{crashln, string}; use std::{collections::BTreeMap, fs::write}; use pmc::{ config::{ self, structs::{Server, Servers}, }, helpers, }; fn save(servers: BTreeMap) { match home::home_dir() { Some(path) => { let path = path.display(); let config_path = format!("{path}/.pmc/servers.toml"); let contents = match toml::to_string(&Servers { servers: Some(servers) }) { Ok(contents) => contents, Err(err) => crashln!("{} Cannot parse servers.\n{}", *helpers::FAIL, string!(err).white()), }; if let Err(err) = write(&config_path, contents) { crashln!("{} Error writing servers.\n{}", *helpers::FAIL, string!(err).white()) } } None => crashln!("{} Impossible to get your home directory", *helpers::FAIL), } } #[derive(Debug)] struct ServerOption { name: String, formatted: String, } impl std::fmt::Display for ServerOption { #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.formatted, f) } } pub fn list(format: &String, log_level: Option) { let servers = config::servers().servers.take().unwrap_or_else(BTreeMap::new); let options: Vec<_> = servers .iter() .map(|(key, server)| { let verbose = match log_level { Some(_) => format!("({})", server.address), None => string!(), }; ServerOption { name: key.clone(), formatted: format!("{} {}", format!("{key}").bright_yellow(), verbose.white()), } }) .collect(); match Select::new("Select a server:", options).prompt() { - Ok(server) => super::list(format, &server.name), + Ok(server) => super::internal::Internal::list(format, &server.name), Err(_) => crashln!("{}", "Canceled...".white()), } } pub fn new() { let (name, address, token); let mut servers = config::servers().servers.take().unwrap_or_else(BTreeMap::new); match Text::new("Server Name:").prompt() { Ok(ans) => name = ans, Err(_) => crashln!("{}", "Canceled...".white()), } match Text::new("Server Address:").prompt() { Ok(ans) => address = ans, Err(_) => crashln!("{}", "Canceled...".white()), } match Password::new("Server Token:") .with_display_toggle_enabled() .with_formatter(&|_| String::from("[hidden]")) .with_display_mode(PasswordDisplayMode::Masked) .without_confirmation() .prompt() { Ok(ans) => match ans.as_str() { "" => token = None, ans => token = Some(string!(ans)), }, Err(_) => crashln!("{}", "Canceled...".white()), } match Confirm::new("Add server? (y/n)").prompt() { Err(_) => crashln!("{}", "Canceled...".white()), Ok(false) => {} Ok(true) => { if name == "" || address == "" { crashln!("{} Failed to add new server", *helpers::FAIL) } else { servers.insert(name, Server { address, token }); save(servers); println!("{} Added new server", *helpers::SUCCESS) } } } } pub fn remove(name: &String) { let mut servers = config::servers().servers.take().unwrap_or_else(BTreeMap::new); if servers.contains_key(name) { match Confirm::new(&format!("Remove server {name}? (y/n)")).prompt() { Err(_) => crashln!("{}", "Canceled...".white()), Ok(false) => {} Ok(true) => { servers.remove(name); save(servers); println!("{} Removed server (name={name})", *helpers::SUCCESS); } } } else { println!("{} Server {name} does not exist", *helpers::FAIL); } } pub fn default(name: &Option) { let servers = config::servers().servers.take().unwrap_or_else(BTreeMap::new); let name = match name { Some(name) => name.as_str(), None => "local", }; if servers.contains_key(name) || name == "internal" || name == "local" { config::read().set_default(string!(name)).save(); println!("{} Set default server to {name}", *helpers::SUCCESS) } else { println!("{} Server {name} does not exist", *helpers::FAIL); } } diff --git a/src/daemon/fork.rs b/src/daemon/fork.rs index 6f8941b..b7589c9 100644 --- a/src/daemon/fork.rs +++ b/src/daemon/fork.rs @@ -1,61 +1,62 @@ use global_placeholders::global; use std::{ffi::CString, process::exit}; +#[allow(dead_code)] pub enum Fork { Parent(libc::pid_t), Child, } pub fn chdir() -> Result { let dir = CString::new(global!("pmc.base")).expect("CString::new failed"); let res = unsafe { libc::chdir(dir.as_ptr()) }; match res { -1 => Err(-1), res => Ok(res), } } pub fn fork() -> Result { let res = unsafe { libc::fork() }; match res { -1 => Err(-1), 0 => Ok(Fork::Child), res => Ok(Fork::Parent(res)), } } pub fn setsid() -> Result { let res = unsafe { libc::setsid() }; match res { -1 => Err(-1), res => Ok(res), } } pub fn close_fd() -> Result { let mut res = false; for i in 0..=2 { res |= unsafe { libc::close(i) } == -1; } return match res { true => Err(-1), false => Ok(1), }; } pub fn daemon(nochdir: bool, noclose: bool) -> Result { match fork() { Ok(Fork::Parent(_)) => exit(0), Ok(Fork::Child) => setsid().and_then(|_| { if !nochdir { chdir()?; } if !noclose { close_fd()?; } fork() }), Err(n) => Err(n), } } diff --git a/src/main.rs b/src/main.rs index b48f573..c9a5f9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,238 +1,223 @@ mod cli; mod daemon; mod globals; mod webui; 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}, + cli::{internal::Internal, Args, Item}, globals::defaults, }; -// migrate to helpers -fn validate_id_script(s: &str) -> Result { - if let Ok(id) = s.parse::() { - Ok(Args::Id(id)) - } else { - Ok(Args::Script(s.to_owned())) - } -} - -// migrate to helpers -fn validate_item(s: &str) -> Result { - if let Ok(id) = s.parse::() { - 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 { None } } #[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(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, }, } // add pmc restore command #[derive(Subcommand)] enum Commands { /// Start/Restart a process #[command(visible_alias = "restart")] Start { /// Process name #[arg(long)] name: Option, - #[clap(value_parser = validate_id_script)] + #[clap(value_parser = cli::validate::)] args: Args, /// Watch to reload path #[arg(long)] watch: Option, /// Server #[arg(short, long)] server: Option, }, /// Stop/Kill a process #[command(visible_alias = "kill")] Stop { - #[clap(value_parser = validate_item)] + #[clap(value_parser = cli::validate::)] item: Item, /// Server #[arg(short, long)] server: Option, }, /// Stop then remove a process #[command(visible_alias = "rm")] Remove { - #[clap(value_parser = validate_item)] + #[clap(value_parser = cli::validate::)] item: Item, /// Server #[arg(short, long)] server: Option, }, /// Get env of a process #[command(visible_alias = "cmdline")] Env { - id: usize, + #[clap(value_parser = cli::validate::)] + item: Item, /// Server #[arg(short, long)] server: Option, }, /// Get information of a process #[command(visible_alias = "info")] Details { - id: usize, + #[clap(value_parser = cli::validate::)] + item: Item, /// Format output #[arg(long, default_value_t = string!("default"))] format: String, /// Server #[arg(short, long)] server: Option, }, /// 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, }, /// Get logs from a process Logs { - id: usize, + #[clap(value_parser = cli::validate::)] + item: Item, #[arg(long, default_value_t = 15, help = "")] lines: usize, /// Server #[arg(short, long)] server: Option, }, /// 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 { 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::Env { item, server } => cli::env(item, &defaults(server)), + Commands::Details { item, format, server } => cli::info(item, format, &defaults(server)), + Commands::List { format, server } => Internal::list(format, &defaults(server)), + Commands::Logs { item, lines, server } => cli::logs(item, 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 7743222..6ea086a 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,576 +1,595 @@ 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, } #[derive(Serialize, Deserialize, ToSchema)] pub struct Stats { pub restarts: u64, pub start_time: i64, pub cpu_percent: Option, pub memory_usage: Option, } #[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, } #[derive(Clone)] pub struct ProcessWrapper { pub id: usize, pub runner: Arc>, } type Env = BTreeMap; #[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, #[serde(with = "ts_milliseconds")] pub started: DateTime, } #[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, pub list: BTreeMap, } #[derive(Clone, Debug)] pub struct Remote { address: String, token: Option, pub config: RemoteConfig, } #[derive(Clone, Debug, Deserialize)] pub struct RemoteConfig { pub shell: String, pub args: Vec, 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) { 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 { 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) -> &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(), }); process.running = true; process.children = vec![]; process.started = Utc::now(); process.crash.crashed = false; process.env = env::vars().collect(); 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 { self.list.clone() } pub fn items_mut(&mut self) -> &mut BTreeMap { &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 { 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 { 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) -> &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 find(&self, name: &str, server_name: &String) -> Option { + let mut runner = self.clone(); + + 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) + }; + } + + runner.list.iter().find(|(_, p)| p.name == name).map(|(id, _)| *id) + } + pub fn fetch(&self) -> Vec { let mut processes: Vec = Vec::new(); for (id, item) in self.items() { let mut memory_usage: Option = None; let mut cpu_percent: Option = 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 = None; let mut cpu_percent: Option = 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;