Page MenuHomePhorge

No OneTemporary

Size
19 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 37d6730..f8c311e 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,50 @@
# Process Management Controller (PMC)
## Overview
PMC (Process Management Controller) is a simple PM2 alternative written in Rust. It provides a command-line interface to start, stop, restart, and manage fork processes
## Features
- Start, stop, and restart processes.
- List all running processes with customizable output formats.
- Retrieve detailed information about a specific process.
## Usage
```bash
# Start/Restart a process
pmc start <id> or <script> [--name <name>]
# Stop/Kill a process
pmc stop <id>
# Remove a process
pmc remove <id>
# Get process info
pmc info <id>
# List all processes
pmc list [--format <raw|json|default>]
# Get process logs
pmc logs <id> [--lines <num_lines>]
```
For more commands, check out `pmc --help`
### Installation
-Pre-built binaries for Linux, MacOS, and Windows can be found on the [releases](releases) page.
+Pre-built binaries for Linux, MacOS, and WSL can be found on the [releases](releases) page. There is no windows support yet.
Install from crates.io using `cargo install pmc`
#### Building
- Clone the project
- Open a terminal in the project folder
- Check if you have cargo (Rust's package manager) installed, just type in `cargo`
- If cargo is installed, run `cargo build --release`
- Put the executable into one of your PATH entries
- Linux: usually /bin/ or /usr/bin/
- Windows: C:\Windows\System32 is good for it but don't use windows
diff --git a/src/cli.rs b/src/cli.rs
index da5ef85..4246a82 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,172 +1,262 @@
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::Rows,
+ object::{Columns, Rows},
style::{BorderColor, Style},
themes::Colorization,
- Color,
+ 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!(""));
}
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);
println!("{} created ({name}) ✓", *helpers::SUCCESS);
list(&string!(""));
}
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!(""));
}
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);
list(&string!(""));
}
pub fn info(id: &usize) {
let runner = Runner::new();
- println!("{:?}", runner.info(*id));
+
+ #[derive(Tabled, Serialize)]
+ 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,
+ }
+
+ 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)
+ .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();
+
+ 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!("{:.1}%", 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 mut table = Table::new(&processes);
- table
+ 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(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}"),
- _ => println!("{}", table.to_string()),
+ _ => println!("{table}"),
};
};
}
}
diff --git a/src/file.rs b/src/file.rs
index f26fdc7..46ae189 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -1,38 +1,45 @@
use crate::helpers;
use anyhow::Error;
use colored::Colorize;
use macros_rs::{crashln, str, ternary};
use std::{
env,
fs::File,
io::{self, BufRead, BufReader},
- path::{Path, PathBuf},
+ path::{Path, PathBuf, StripPrefixError},
};
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()) }
}
diff --git a/src/main.rs b/src/main.rs
index f2a5886..1879b45 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,129 +1,134 @@
mod cli;
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 macros_rs::{str, string};
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 {
/// Start all processes
StartAll,
/// Stop all processes
StopAll,
/// Reset process index
ResetIndex,
/// Check daemon
Health,
}
// 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 },
/// List all processes
#[command(alias = "ls")]
List {
#[arg(long, default_value_t = string!(""), help = "format output")]
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() {
// make sure process is running, if not, restart
// make this daemon based.
let cli = Cli::parse();
globals::init();
env_logger::Builder::new().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 {
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 } => cli::info(id),
Commands::List { format } => cli::list(format),
Commands::Logs { id, lines } => cli::logs(id, lines),
Commands::Daemon { command } => match command {
Daemon::StartAll => {}
Daemon::StopAll => {}
Daemon::ResetIndex => {}
Daemon::Health => {}
},
}
}
#[cxx::bridge]
pub mod service {
unsafe extern "C++" {
include!("pmc/src/include/process.h");
include!("pmc/src/include/bridge.h");
pub fn stop(pid: i64) -> i64;
pub fn run(name: &str, log_path: &str, command: &str) -> i64;
}
}
diff --git a/src/process/mod.rs b/src/process/mod.rs
index 785a928..4ae86d3 100644
--- a/src/process/mod.rs
+++ b/src/process/mod.rs
@@ -1,113 +1,119 @@
mod dump;
use crate::file;
use crate::helpers::{self, Id};
use crate::service::{run, stop};
use chrono::serde::ts_milliseconds;
use chrono::{DateTime, Utc};
use macros_rs::{crashln, string};
use serde::{Deserialize, Serialize};
-use std::collections::BTreeMap;
+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);
self.process_list.insert(
string!(self.id.next()),
Process {
pid,
name,
+ env: env::vars().collect(),
path: file::cwd(),
started: Utc::now(),
script: string!(command),
running: true,
},
);
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);
self.process_list.insert(
string!(id),
Process {
pid,
name,
path,
+ env,
script,
started: Utc::now(),
running: true,
},
);
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 }
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Mar 19, 10:56 PM (11 h, 11 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
506938
Default Alt Text
(19 KB)

Event Timeline