diff --git a/build.rs b/build.rs index 502b1b1..8cb0b9d 100644 --- a/build.rs +++ b/build.rs @@ -1,38 +1,39 @@ use chrono::Datelike; use std::{env, process::Command}; fn main() { #[cfg(windows)] { println!("cargo:warning=This project is not supported on Windows."); std::process::exit(1); } /* 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" => println!("cargo:rustc-env=PROFILE=release"), _ => println!("cargo:rustc-env=PROFILE=none"), } /* cc linking */ cxx_build::bridge("src/main.rs") .file("src/cc/bridge.cc") .file("src/cc/process.cc") - .flag_if_supported("-std=c++14") + .file("src/cc/fork.cc") + .flag_if_supported("-std=c++17") .compile("bridge"); - let watched = vec!["main.rs", "cc/bridge.cc", "cc/process.cc", "include/process.h"]; + let watched = vec!["main.rs", "cc/bridge.cc", "cc/process.cc", "cc/fork.cc", "include/bridge.h", "include/process.h", "include/fork.h"]; watched.iter().for_each(|file| println!("cargo:rerun-if-changed=src/{}", file)); } diff --git a/src/cc/bridge.cc b/src/cc/bridge.cc index e1786a0..57d337d 100644 --- a/src/cc/bridge.cc +++ b/src/cc/bridge.cc @@ -1,13 +1,17 @@ #include "../include/bridge.h" -#include "process.cc" +#include "../include/process.h" + #include <signal.h> +#include <iostream> +#include <string> +using namespace std; int64_t stop(int64_t pid) { return kill(pid, SIGTERM); } -int64_t run(Str name, Str log_path, Str command) { - process::Runner runner; - runner.New(std::string(name), std::string(log_path)); - return runner.Run(std::string(command)); +int64_t run(ProcessMetadata metadata) { + process::Runner runner; + runner.New(std::string(metadata.name), std::string(metadata.log_path)); + return runner.Run(std::string(metadata.command), std::string(metadata.shell), metadata.args); } \ No newline at end of file diff --git a/src/cc/fork.cc b/src/cc/fork.cc new file mode 100644 index 0000000..322ad5e --- /dev/null +++ b/src/cc/fork.cc @@ -0,0 +1,82 @@ +#include "../include/fork.h" +#include <cstring> +#include <stdexcept> +#include <cstdlib> +#include <iostream> +#include <unistd.h> + +#ifdef _WIN32 +#include <windows.h> +#else +#include <pwd.h> +#include <unistd.h> +#endif + +std::string home() { + #ifdef _WIN32 + const char* userProfile = std::getenv("USERPROFILE"); + if (userProfile) { + return std::string(userProfile); + } else { + return ""; + } + #else + struct passwd* pw = getpwuid(getuid()); + if (pw && pw->pw_dir) { + return std::string(pw->pw_dir); + } else { + return ""; + } + #endif +} + + +Fork fork_process() { + pid_t res = ::fork(); + if (res == -1) { + throw std::runtime_error("fork() failed"); + } else if (res == 0) { + return Fork::Child; + } else { + return Fork::Parent; + } +} + +pid_t set_sid() { + pid_t res = ::setsid(); + if (res == -1) { + throw std::runtime_error("setsid() failed"); + } + return res; +} + +void close_fd() { + if (::close(0) == -1 || ::close(1) == -1 || ::close(2) == -1) { + throw std::runtime_error("close() failed"); + } +} + +int32_t try_fork(bool nochdir, bool noclose, Callback callback) { + try { + Fork forkResult = fork_process(); + if (forkResult == Fork::Parent) { + exit(0); + } else if (forkResult == Fork::Child) { + set_sid(); + if (!nochdir) { + std::string home_dir = home() + ".pmc"; + chdir(home_dir.c_str()); + } + if (!noclose) { + close_fd(); + } + forkResult = fork_process(); + } + return static_cast<int32_t>(forkResult); + } catch (const std::exception& e) { + std::cerr << "[PMC] (cc) Error setting up daemon handler\n"; + } + + callback(); + return -1; +} \ No newline at end of file diff --git a/src/cc/process.cc b/src/cc/process.cc index bd52079..a92024c 100644 --- a/src/cc/process.cc +++ b/src/cc/process.cc @@ -1,73 +1,84 @@ #include "../include/process.h" #include <fcntl.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <iostream> +using namespace std; namespace process { volatile sig_atomic_t childExitStatus = 0; +pair<std::string, std::string> split(const std::string& str) { + size_t length = str.length(); + size_t midpoint = length / 2; + + std::string firstHalf = str.substr(0, midpoint); + std::string secondHalf = str.substr(midpoint); + + return make_pair(firstHalf, secondHalf); +} + void sigchld_handler(int signo) { (void)signo; int status; while (waitpid(-1, &status, WNOHANG) > 0) { childExitStatus = status; } } void Runner::New(const std::string &name, const std::string &logPath) { std::string stdoutFileName = logPath + "/" + name + "-out.log"; std::string stderrFileName = logPath + "/" + name + "-error.log"; stdout_fd = open(stdoutFileName.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644); stderr_fd = open(stderrFileName.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644); struct sigaction sa; sa.sa_handler = sigchld_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; if (sigaction(SIGCHLD, &sa, NULL) == -1) { std::cerr << "[PMC] (cc) Error setting up SIGCHLD handler\n"; } } Runner::~Runner() { if (stdout_fd != -1) { close(stdout_fd); } if (stderr_fd != -1) { close(stderr_fd); } } -int64_t Runner::Run(const std::string &command) { +int64_t Runner::Run(const std::string &command, const std::string &shell, Vec<String> args) { pid_t pid = fork(); if (pid == -1) { std::cerr << "[PMC] (cc) Unable to fork\n"; return -1; } else if (pid == 0) { setsid(); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); dup2(stdout_fd, STDOUT_FILENO); dup2(stderr_fd, STDERR_FILENO); - if (execl("/bin/bash", "bash", "-c", command.c_str(), (char *)nullptr) == -1) { + if (execl(shell.c_str(), args[0].c_str(), args[1].c_str(), command.c_str(), (char *)nullptr) == -1) { std::cerr << "[PMC] (cc) Unable to execute the command\n"; exit(EXIT_FAILURE); } } else { close(stdout_fd); close(stderr_fd); return pid; } return -1; }} diff --git a/src/cli.rs b/src/cli.rs index 237efc7..4bfe10c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,289 +1,289 @@ use crate::file; use crate::helpers::{self, ColoredString}; use crate::process::Runner; use crate::structs::Args; 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 tabled::{ settings::{ object::{Columns, Rows}, style::{BorderColor, Style}, themes::Colorization, Color, Rotate, }, Table, Tabled, }; 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>) { let mut runner = Runner::new(); match args { Some(Args::Id(id)) => { println!("{} Applying action restartProcess on ({id})", *helpers::SUCCESS); runner.restart(*id, name); println!("{} restarted ({id}) ✓", *helpers::SUCCESS); - list(&string!("")); + list(&string!("default")); } Some(Args::Script(script)) => { let name = match name { Some(name) => string!(name), None => string!(script.split_whitespace().next().unwrap_or_default()), }; println!("{} Creating process with ({name})", *helpers::SUCCESS); - runner.start(name.clone(), script); + runner.start(&name, script); println!("{} created ({name}) ✓", *helpers::SUCCESS); - list(&string!("")); + list(&string!("default")); } None => {} } } pub fn stop(id: &usize) { println!("{} Applying action stopProcess on ({id})", *helpers::SUCCESS); let mut runner = Runner::new(); runner.stop(*id); println!("{} stopped ({id}) ✓", *helpers::SUCCESS); - list(&string!("")); + list(&string!("default")); } pub fn remove(id: &usize) { println!("{} Applying action removeProcess on ({id})", *helpers::SUCCESS); let mut runner = Runner::new(); runner.remove(*id); println!("{} removed ({id}) ✓", *helpers::SUCCESS); } pub fn info(id: &usize, format: &String) { let runner = Runner::new(); #[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 = "script command ")] command: String, #[tabled(rename = "exec cwd")] path: String, #[tabled(rename = "script id")] id: String, 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(), "name": &self.name.trim(), "pid": &self.pid.trim(), "uptime": &self.uptime.trim(), "status": &self.status.0.trim(), "log_error": &self.log_error.trim(), "log_out": &self.log_out.trim(), "cpu": &self.cpu_percent.trim(), "mem": &self.memory_usage.trim(), "command": &self.command.trim(), "path": &self.path.trim(), }); trimmed_json.serialize(serializer) } } if let Some(home) = home::home_dir() { if let Some(item) = runner.info(*id) { 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 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), name: item.name.clone(), command: format!("/bin/bash -c '{}'", item.script.clone()), path: format!("{} ", path), 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")), status: ColoredString(ternary!(item.running, "online".green().bold(), "stopped".red().bold())), 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!("{} Process ({id}) not found", *helpers::FAIL); } } else { crashln!("{} Impossible to get your home directory", *helpers::FAIL); } } pub fn logs(id: &usize, lines: &usize) { let runner = Runner::new(); if let Some(item) = runner.info(*id) { println!("{}", format!("Showing last {lines} lines for process [{id}] (change the value with --lines option)").yellow()); let log_error = global!("pmc.logs.error", item.name.as_str()); let log_out = global!("pmc.logs.out", item.name.as_str()); file::logs(*lines, &log_error, *id, "error", &item.name); file::logs(*lines, &log_out, *id, "out", &item.name); } else { crashln!("{} Process ({id}) not found", *helpers::FAIL); } } #[cfg(target_os = "macos")] pub fn env(id: &usize) { let runner = Runner::new(); if let Some(item) = runner.info(*id) { for (key, value) in item.env.iter() { println!("{}: {}", key, value.green()); } } else { crashln!("{} Process ({id}) not found", *helpers::FAIL); } } pub fn list(format: &String) { let runner = Runner::new(); let mut processes: Vec<ProcessItem> = Vec::new(); #[derive(Tabled, Debug)] struct ProcessItem { id: ColoredString, name: String, pid: String, uptime: String, status: ColoredString, cpu: String, mem: String, } impl serde::Serialize for ProcessItem { fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { let trimmed_json = json!({ "id": &self.id.0.trim(), "name": &self.name.trim(), "pid": &self.pid.trim(), "uptime": &self.uptime.trim(), "status": &self.status.0.trim(), "cpu": &self.cpu.trim(), "mem": &self.mem.trim(), }); trimmed_json.serialize(serializer) } } if runner.list().is_empty() { println!("{} Process table empty", *helpers::SUCCESS); } else { for (id, item) in runner.list() { 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"), }; processes.push(ProcessItem { id: ColoredString(id.cyan().bold()), pid: ternary!(item.running, format!("{} ", item.pid), string!("n/a ")), cpu: format!("{cpu_percent} "), mem: format!("{memory_usage} "), name: format!("{} ", item.name.clone()), status: ColoredString(ternary!(item.running, "online ".green().bold(), "stopped ".red().bold())), 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())) .to_string(); if let Ok(json) = serde_json::to_string(&processes) { match format.as_str() { "raw" => println!("{:?}", processes), "json" => println!("{json}"), "default" => println!("{table}"), _ => {} }; }; } } diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..10cc14b --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,45 @@ +pub mod structs; + +use crate::file::{self, Exists}; +use crate::helpers; + +use colored::Colorize; +use macros_rs::{crashln, string}; +use std::fs; +use structs::{Config, Daemon, Runner}; + +pub fn read() -> Config { + match home::home_dir() { + Some(path) => { + let path = path.display(); + let config_path = format!("{path}/.pmc/config.toml"); + + if !Exists::file(config_path.clone()).unwrap() { + let config = Config { + runner: Runner { + shell: string!("/bin/bash"), + args: vec![string!("bash"), string!("-c")], + log_path: format!("{path}/.pmc/logs"), + }, + daemon: Daemon { + interval: 1000, + kind: string!("default"), + }, + }; + + let contents = match toml::to_string(&config) { + Ok(contents) => contents, + Err(err) => crashln!("{} Cannot parse config.\n{}", *helpers::FAIL, string!(err).white()), + }; + + if let Err(err) = fs::write(&config_path, contents) { + crashln!("{} Error writing config.\n{}", *helpers::FAIL, string!(err).white()) + } + log::info!("created config file"); + } + + file::read(config_path) + } + None => crashln!("{} Impossible to get your home directory", *helpers::FAIL), + } +} diff --git a/src/config/structs.rs b/src/config/structs.rs new file mode 100644 index 0000000..a3d8a0e --- /dev/null +++ b/src/config/structs.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Config { + pub runner: Runner, + pub daemon: Daemon, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Runner { + pub shell: String, + pub args: Vec<String>, + pub log_path: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Daemon { + pub interval: u64, + pub kind: String, +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 74a7aa8..2f49271 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -1,199 +1,217 @@ pub mod fork; pub mod pid; +use crate::config; use crate::helpers::{self, ColoredString}; use crate::process::Runner; use crate::service; use chrono::{DateTime, Utc}; use colored::Colorize; use fork::{daemon, Fork}; use global_placeholders::global; use macros_rs::{crashln, string, ternary, then}; use psutil::process::{MemoryInfo, Process}; use serde::Serialize; use serde_json::json; use std::{process, thread::sleep, time::Duration}; use tabled::{ settings::{ object::Columns, style::{BorderColor, Style}, themes::Colorization, Color, Rotate, }, Table, Tabled, }; extern "C" fn handle_termination_signal(_: libc::c_int) { pid::remove(); unsafe { libc::_exit(0) } } fn restart_process(runner: Runner) { let items = runner.list().iter().filter_map(|(id, item)| Some((id.trim().parse::<usize>().ok()?, item))); for (id, item) in items { then!(!item.running || pid::running(item.pid as i32), continue); let name = &Some(item.name.clone()); let mut runner_instance = Runner::new(); runner_instance.restart(id, name); } } pub fn health(format: &String) { let runner = Runner::new(); let mut pid: Option<i32> = None; let mut cpu_percent: Option<f32> = None; let mut uptime: Option<DateTime<Utc>> = None; let mut memory_usage: Option<MemoryInfo> = None; #[derive(Clone, Debug, Tabled)] struct Info { #[tabled(rename = "pid file")] pid_file: String, #[tabled(rename = "fork path")] path: String, #[tabled(rename = "cpu percent")] cpu_percent: String, #[tabled(rename = "memory usage")] memory_usage: String, + #[tabled(rename = "daemon type")] + external: String, #[tabled(rename = "process count")] process_count: usize, uptime: String, pid: String, status: ColoredString, } impl Serialize for Info { fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { let trimmed_json = json!({ "pid_file": &self.pid_file.trim(), "path": &self.path.trim(), "cpu": &self.cpu_percent.trim(), "mem": &self.memory_usage.trim(), "process_count": &self.process_count.to_string(), "uptime": &self.uptime.trim(), "pid": &self.pid.trim(), "status": &self.status.0.trim(), }); trimmed_json.serialize(serializer) } } if pid::exists() { if let Ok(process_id) = pid::read() { if let Ok(mut process) = Process::new(process_id as u32) { pid = Some(process_id); uptime = Some(pid::uptime().unwrap()); 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 uptime = match uptime { Some(uptime) => helpers::format_duration(uptime), None => string!("none"), }; let pid = match pid { Some(pid) => string!(pid), None => string!("n/a"), }; let data = vec![Info { pid: pid, cpu_percent, memory_usage, uptime: uptime, path: global!("pmc.base"), - pid_file: format!("{} ", global!("pmc.pid")), + external: global!("pmc.daemon.kind"), process_count: runner.list().keys().len(), + pid_file: format!("{} ", global!("pmc.pid")), status: ColoredString(ternary!(pid::exists(), "online".green().bold(), "stopped".red().bold())), }]; 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}"), "default" => { println!("{}\n{table}\n", format!("PMC daemon information").on_bright_white().black()); println!(" {}", format!("Use `pmc daemon restart` to restart the daemon").white()); println!(" {}", format!("Use `pmc daemon reset` to clean process id values").white()); } _ => {} }; }; } pub fn stop() { if pid::exists() { println!("{} Stopping PMC daemon", *helpers::SUCCESS); match pid::read() { Ok(pid) => { service::stop(pid as i64); pid::remove(); println!("{} PMC daemon stopped", *helpers::SUCCESS); } Err(err) => crashln!("{} Failed to read PID file: {}", *helpers::FAIL, err), } } else { crashln!("{} The daemon is not running", *helpers::FAIL) } } pub fn start() { + let external = match global!("pmc.daemon.kind").as_str() { + "external" => true, + "default" => false, + "rust" => false, + "cc" => true, + _ => false, + }; + pid::name("PMC Restart Handler Daemon"); - println!("{} Spawning PMC daemon with pmc_home={}", *helpers::SUCCESS, global!("pmc.base")); + println!("{} Spawning PMC daemon (pmc_base={})", *helpers::SUCCESS, global!("pmc.base")); if pid::exists() { match pid::read() { Ok(pid) => then!(!pid::running(pid), pid::remove()), Err(_) => crashln!("{} The daemon is already running", *helpers::FAIL), } } - println!("{} PMC Successfully daemonized", *helpers::SUCCESS); - match daemon(false, false) { - Ok(Fork::Parent(_)) => {} - Ok(Fork::Child) => { - unsafe { libc::signal(libc::SIGTERM, handle_termination_signal as usize) }; - pid::write(process::id()); - - loop { - let runner = Runner::new(); - then!(!runner.list().is_empty(), restart_process(runner)); - sleep(Duration::from_secs(1)); - } + extern "C" fn init() { + let config = config::read(); + unsafe { libc::signal(libc::SIGTERM, handle_termination_signal as usize) }; + pid::write(process::id()); + + loop { + let runner = Runner::new(); + then!(!runner.list().is_empty(), restart_process(runner)); + sleep(Duration::from_millis(config.daemon.interval)); } - Err(err) => { - crashln!("{} Daemon creation failed with code {err}", *helpers::FAIL) + } + + println!("{} PMC Successfully daemonized (type={})", *helpers::SUCCESS, global!("pmc.daemon.kind")); + if external { + let callback = crate::Callback(init); + crate::service::try_fork(false, false, callback); + } else { + match daemon(false, false) { + Ok(Fork::Parent(_)) => {} + Ok(Fork::Child) => init(), + Err(err) => crashln!("{} Daemon creation failed with code {err}", *helpers::FAIL), } } } pub fn restart() { if pid::exists() { stop(); } start(); } diff --git a/src/file.rs b/src/file.rs index 46ae189..ab525aa 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,45 +1,84 @@ use crate::helpers; use anyhow::Error; use colored::Colorize; -use macros_rs::{crashln, str, ternary}; +use macros_rs::{crashln, str, string, ternary}; use std::{ env, - fs::File, + fs::{self, File}, io::{self, BufRead, BufReader}, path::{Path, PathBuf, StripPrefixError}, + thread::sleep, + time::Duration, }; pub fn logs(lines_to_tail: usize, log_file: &str, id: usize, log_type: &str, item_name: &str) { let file = File::open(log_file).unwrap(); let reader = BufReader::new(file); let lines: Vec<String> = reader.lines().collect::<io::Result<_>>().unwrap(); let color = ternary!(log_type == "out", "green", "red"); println!("{}", format!("\n{log_file} last {lines_to_tail} lines:").bright_black()); let start_index = if lines.len() > lines_to_tail { lines.len() - lines_to_tail } else { 0 }; for (_, line) in lines.iter().skip(start_index).enumerate() { println!("{} {}", format!("{}|{} |", id, item_name).color(color), line); } } pub fn cwd() -> PathBuf { match env::current_dir() { Ok(path) => path, Err(_) => crashln!("{} Unable to find current working directory", *helpers::FAIL), } } pub fn make_relative(current: &Path, home: &Path) -> Option<std::path::PathBuf> { match current.strip_prefix(home) { Ok(relative_path) => Some(Path::new("~").join(relative_path)), Err(StripPrefixError { .. }) => None, } } pub struct Exists; impl Exists { pub fn folder(dir_name: String) -> Result<bool, Error> { Ok(Path::new(str!(dir_name)).is_dir()) } pub fn file(file_name: String) -> Result<bool, Error> { Ok(Path::new(str!(file_name)).exists()) } } + +pub fn read<T: serde::de::DeserializeOwned>(path: String) -> T { + let mut retry_count = 0; + let max_retries = 5; + + let contents = loop { + match fs::read_to_string(&path) { + Ok(contents) => break contents, + Err(err) => { + retry_count += 1; + if retry_count >= max_retries { + crashln!("{} Cannot find dumpfile.\n{}", *helpers::FAIL, string!(err).white()); + } else { + println!("{} Error reading dumpfile. Retrying... (Attempt {}/{})", *helpers::FAIL, retry_count, max_retries); + } + } + } + sleep(Duration::from_secs(1)); + }; + + retry_count = 0; + + loop { + match toml::from_str(&contents).map_err(|err| string!(err)) { + Ok(parsed) => break parsed, + Err(err) => { + retry_count += 1; + if retry_count >= max_retries { + crashln!("{} Cannot parse dumpfile.\n{}", *helpers::FAIL, err.white()); + } else { + println!("{} Error parsing dumpfile. Retrying... (Attempt {}/{})", *helpers::FAIL, retry_count, max_retries); + } + } + } + sleep(Duration::from_secs(1)); + } +} diff --git a/src/globals.rs b/src/globals.rs index 60c7bbf..6d497c0 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -1,26 +1,43 @@ +use crate::config; +use crate::file::Exists; use crate::helpers; + use global_placeholders::init; use macros_rs::crashln; +use std::fs; pub fn init() { match home::home_dir() { Some(path) => { - let base = format!("{}/.pmc/", path.display()); - let logs = format!("{}/.pmc/logs/", path.display()); - let pid = format!("{}/.pmc/daemon.pid", path.display()); - let dump = format!("{}/.pmc/dump.toml", path.display()); - - init!("pmc.pid", pid); - init!("pmc.base", base); - init!("pmc.logs", logs); - init!("pmc.dump", dump); - - let out = format!("{logs}{{}}-out.log"); - let error = format!("{logs}{{}}-error.log"); + let path = path.display(); + if !Exists::folder(format!("{path}/.pmc/")).unwrap() { + fs::create_dir_all(format!("{path}/.pmc/")).unwrap(); + log::info!("created pmc base dir"); + } + + let config = config::read(); + if !Exists::folder(config.runner.log_path.clone()).unwrap() { + fs::create_dir_all(&config.runner.log_path).unwrap(); + log::info!("created pmc log dir"); + } + + init!("pmc.base", format!("{path}/.pmc/")); + init!("pmc.pid", format!("{path}/.pmc/daemon.pid")); + init!("pmc.dump", format!("{path}/.pmc/process.dump")); + + init!("pmc.config.shell", config.runner.shell); + init!("pmc.config.log_path", config.runner.log_path); + + init!("pmc.daemon.kind", config.daemon.kind); + init!("pmc.daemon.interval", config.daemon.interval); + init!("pmc.daemon.logs", format!("{path}/.pmc/daemon.log")); + + let out = format!("{}/{{}}-out.log", config.runner.log_path); + let error = format!("{}/{{}}-error.log", config.runner.log_path); init!("pmc.logs.out", out); init!("pmc.logs.error", error); } None => crashln!("{} Impossible to get your home directory", *helpers::FAIL), } } diff --git a/src/include/bridge.h b/src/include/bridge.h index b0b769c..7041631 100644 --- a/src/include/bridge.h +++ b/src/include/bridge.h @@ -1,10 +1,21 @@ -#ifndef bridge -#define bridge +#ifndef BRIDGE_H +#define BRIDGE_H #include "rust.h" using namespace rust; -int64_t stop(int64_t pid); -int64_t run(Str name, Str log_path, Str command); +#ifndef CXXBRIDGE1_STRUCT_ProcessMetadata +#define CXXBRIDGE1_STRUCT_ProcessMetadata +struct ProcessMetadata final { + String name; + String shell; + String command; + String log_path; + Vec<String> args; + using IsRelocatable = std::true_type; +}; +#endif -#endif \ No newline at end of file +extern "C" int64_t stop(int64_t pid); +extern "C" int64_t run(ProcessMetadata metadata); +#endif diff --git a/src/include/fork.h b/src/include/fork.h new file mode 100644 index 0000000..825405d --- /dev/null +++ b/src/include/fork.h @@ -0,0 +1,19 @@ +#ifndef FORK_H +#define FORK_H +#include <string> + +#ifndef CXXBRIDGE1_ENUM_Fork +#define CXXBRIDGE1_ENUM_Fork +enum class Fork: uint8_t { + Parent, + Child +}; +#endif + +using Callback = void(*)(); +extern "C" pid_t set_sid(); +extern "C" void close_fd(); +extern "C" Fork fork_process(); +extern "C" int chdir(const char* dir); +extern "C" int32_t try_fork(bool nochdir, bool noclose, Callback callback); +#endif diff --git a/src/include/process.h b/src/include/process.h index b7a168c..3947a37 100644 --- a/src/include/process.h +++ b/src/include/process.h @@ -1,20 +1,20 @@ -#ifndef process -#define process +#ifndef PROCESS_H +#define PROCESS_H #include "rust.h" using namespace rust; namespace process { class Runner { public: void New(const std::string &name, const std::string &logPath); - int64_t Run(const std::string &command); + int64_t Run(const std::string &command, const std::string &shell, Vec<String> args); ~Runner(); private: int stdout_fd; int stderr_fd; }; } #endif diff --git a/src/main.rs b/src/main.rs index 2f0cd56..3ffc003 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,153 +1,173 @@ mod cli; +mod config; mod daemon; mod file; mod globals; mod helpers; mod process; mod structs; -use crate::file::Exists; use crate::structs::Args; - use clap::{Parser, Subcommand}; use clap_verbosity_flag::Verbosity; -use global_placeholders::global; +use cxx::{type_id, ExternType}; 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, /// Check daemon #[command(alias = "info")] 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 { #[arg(long, help = "process name")] name: Option<String>, #[clap(value_parser = validate_id_script)] args: Option<Args>, }, /// Stop/Kill a process #[command(alias = "kill")] Stop { id: usize }, /// Stop then remove a process #[command(alias = "rm")] Remove { id: usize }, /// Get env of a process #[command(alias = "cmdline")] Env { id: usize }, /// Get information of a process #[command(alias = "info")] Details { id: usize, /// Format output #[arg(long, default_value_t = string!("default"))] format: String, }, /// List all processes #[command(alias = "ls")] List { /// Format output #[arg(long, default_value_t = string!("default"))] format: String, }, /// Get logs from a process Logs { id: usize, #[arg(long, default_value_t = 15, help = "")] lines: usize, }, /// Daemon management Daemon { #[command(subcommand)] command: Daemon, }, } fn main() { globals::init(); let cli = Cli::parse(); let mut env = env_logger::Builder::new(); env.filter_level(cli.verbose.log_level_filter()).init(); - if !Exists::folder(global!("pmc.logs")).unwrap() { - std::fs::create_dir_all(global!("pmc.logs")).unwrap(); - log::info!("Created PMC log directory"); - } - match &cli.command { // add --watch Commands::Start { name, args } => cli::start(name, args), 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::Daemon { command } => match command { Daemon::Reset => {} Daemon::Stop => daemon::stop(), Daemon::Restore => daemon::restart(), Daemon::Health { format } => daemon::health(format), }, }; if !matches!(&cli.command, Commands::Daemon { .. }) { then!(!daemon::pid::exists(), daemon::start()); } } +#[repr(transparent)] +pub struct Callback(pub extern "C" fn()); + +unsafe impl ExternType for Callback { + type Id = type_id!("Callback"); + type Kind = cxx::kind::Trivial; +} + #[cxx::bridge] pub mod service { + + #[repr(u8)] + enum Fork { + Parent, + Child, + } + + pub struct ProcessMetadata { + pub name: String, + pub shell: String, + pub command: String, + pub log_path: String, + pub args: Vec<String>, + } + unsafe extern "C++" { include!("pmc/src/include/process.h"); include!("pmc/src/include/bridge.h"); + include!("pmc/src/include/fork.h"); + type Callback = crate::Callback; pub fn stop(pid: i64) -> i64; - pub fn run(name: &str, log_path: &str, command: &str) -> i64; + pub fn run(metadata: ProcessMetadata) -> i64; + pub fn try_fork(nochdir: bool, noclose: bool, callback: Callback) -> i32; } } diff --git a/src/process/dump.rs b/src/process/dump.rs index 904a56a..017489a 100644 --- a/src/process/dump.rs +++ b/src/process/dump.rs @@ -1,77 +1,33 @@ -use crate::file::Exists; +use crate::file::{self, Exists}; use crate::helpers::{self, Id}; use crate::process::Runner; use colored::Colorize; use global_placeholders::global; use macros_rs::{crashln, string}; -use std::{collections::BTreeMap, fs, thread::sleep, time::Duration}; +use std::{collections::BTreeMap, fs}; pub fn read() -> Runner { - if !Exists::folder(global!("pmc.base")).unwrap() { - fs::create_dir_all(global!("pmc.base")).unwrap(); - log::info!("created pmc base dir"); - } - if !Exists::file(global!("pmc.dump")).unwrap() { let runner = Runner { id: Id::new(0), - log_path: global!("pmc.logs"), process_list: BTreeMap::new(), }; write(&runner); log::info!("created dump file"); } - let mut retry_count = 0; - let max_retries = 5; - - let contents = loop { - match fs::read_to_string(global!("pmc.dump")) { - Ok(contents) => break contents, - Err(err) => { - retry_count += 1; - if retry_count >= max_retries { - crashln!("{} Cannot find dumpfile.\n{}", *helpers::FAIL, string!(err).white()); - } else { - println!("{} Error reading dumpfile. Retrying... (Attempt {}/{})", *helpers::FAIL, retry_count, max_retries); - } - } - } - sleep(Duration::from_secs(1)); - }; - - retry_count = 0; - - loop { - match toml::from_str(&contents).map_err(|err| string!(err)) { - Ok(parsed) => break parsed, - Err(err) => { - retry_count += 1; - if retry_count >= max_retries { - crashln!("{} Cannot parse dumpfile.\n{}", *helpers::FAIL, err.white()); - } else { - println!("{} Error parsing dumpfile. Retrying... (Attempt {}/{})", *helpers::FAIL, retry_count, max_retries); - } - } - } - sleep(Duration::from_secs(1)); - } + file::read(global!("pmc.dump")) } pub fn write(dump: &Runner) { - if !Exists::folder(global!("pmc.base")).unwrap() { - fs::create_dir_all(global!("pmc.base")).unwrap(); - log::info!("created pmc base dir"); - } - let contents = match toml::to_string(dump) { Ok(contents) => contents, Err(err) => crashln!("{} Cannot parse dump.\n{}", *helpers::FAIL, string!(err).white()), }; if let Err(err) = fs::write(global!("pmc.dump"), contents) { crashln!("{} Error writing dumpfile.\n{}", *helpers::FAIL, string!(err).white()) } } diff --git a/src/process/mod.rs b/src/process/mod.rs index 4ae86d3..767e30d 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,119 +1,134 @@ mod dump; +use crate::config; use crate::file; use crate::helpers::{self, Id}; -use crate::service::{run, stop}; +use crate::service::{run, stop, ProcessMetadata}; use chrono::serde::ts_milliseconds; use chrono::{DateTime, Utc}; use macros_rs::{crashln, string}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::env; use std::path::PathBuf; #[derive(Debug, Deserialize, Serialize)] pub struct Process { 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 running: bool, } #[derive(Debug, Deserialize, Serialize)] pub struct Runner { id: Id, - log_path: String, process_list: BTreeMap<String, Process>, } impl Runner { pub fn new() -> Self { let dump = dump::read(); let runner = Runner { id: dump.id, - log_path: dump.log_path, process_list: dump.process_list, }; dump::write(&runner); return runner; } - pub fn start(&mut self, name: String, command: &String) { - let pid = run(&name, &self.log_path, &command); + pub fn start(&mut self, name: &String, command: &String) { + let config = config::read().runner; + let pid = run(ProcessMetadata { + name: name.clone(), + log_path: config.log_path, + command: command.clone(), + shell: config.shell, + args: config.args, + }); + self.process_list.insert( string!(self.id.next()), Process { pid, - name, - env: env::vars().collect(), + running: true, path: file::cwd(), + name: name.clone(), started: Utc::now(), - script: string!(command), - running: true, + script: command.clone(), + env: env::vars().collect(), }, ); dump::write(&self); } pub fn stop(&mut self, id: usize) { if let Some(item) = self.process_list.get_mut(&string!(id)) { stop(item.pid); item.running = false; dump::write(&self); } else { crashln!("{} Process ({id}) not found", *helpers::FAIL); } } pub fn restart(&mut self, id: usize, name: &Option<String>) { if let Some(item) = self.info(id) { let script = item.script.clone(); let path = item.path.clone(); let env = item.env.clone(); let name = match name { Some(name) => string!(name.trim()), None => string!(item.name.clone()), }; if let Err(err) = std::env::set_current_dir(&path) { crashln!("{} Failed to set working directory {:?}\nError: {:#?}", *helpers::FAIL, path, err); }; self.stop(id); - let pid = run(&name, &self.log_path, &script); + + let config = config::read().runner; + let pid = run(ProcessMetadata { + name: name.clone(), + log_path: config.log_path, + command: script.clone(), + shell: config.shell, + args: config.args, + }); self.process_list.insert( string!(id), Process { pid, + env, name, path, - env, script, - started: Utc::now(), running: true, + started: Utc::now(), }, ); dump::write(&self); } else { crashln!("{} Failed to restart process ({})", *helpers::FAIL, id); } } pub fn remove(&mut self, id: usize) { self.stop(id); self.process_list.remove(&string!(id)); dump::write(&self); } pub fn info(&self, id: usize) -> Option<&Process> { self.process_list.get(&string!(id)) } pub fn list(&self) -> &BTreeMap<String, Process> { &self.process_list } }