First commit

This commit is contained in:
gaofei 2025-01-13 17:08:11 +08:00
commit 138803713a
15 changed files with 817 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "pmr"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "A process manager in Rust"
license = "MIT"
[dependencies]
clap = { version = "4.0", features = ["derive"] }
sysinfo = "0.33.1"
tabled = "0.17.0"
serde = { version = "1.0", features = ["derive"] }
dirs = "5.0"
serde_json = "1.0"
crossterm = "0.28.1"
once_cell = "1.18.0"

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Process Manager in Rust
## Usage
Just like pm2
```bash
pmr start --name [name] [program] -- [args]
pmr list/ls
pmr stop [id;name]
pmr stop [id;name]
pmr restart [id;name]
pmr delete/rm [idname]
```

50
instruction.md Normal file
View File

@ -0,0 +1,50 @@
# AI Instruction
1.
在当前文件夹下创建rust工程编写 pmr 命令行工具,用于守护进程的管理:
1. 支持windowns、Linux、MacOS
2. 支持命令直接启动,例如 pmr start --name [别名] [程序名] -- [参数]
3. 支持配置文件启动,例如 pmr start --config config.json:
{
"name": "test",
"program": "python",
"args": ["-m", "test"]
}
4. 支持查看进程列表,例如 pmr listalias命令ls在终端显示列表例如┌────┬───────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ naive │ default │ N/A │ fork │ 1492097 │ 22h │ 39 │ online │ 0% │ 8.1mb │ root │ disabled │
│ 1 │ naive-http │ default │ N/A │ fork │ 1492102 │ 22h │ 9 │ online │ 0% │ 8.3mb │ root │ disabled │
└────┴───────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
5.
每次执行pmr命令检查用户目录下是否有.pmr 文件夹如果没有则创建并在文件夹下创建dump.json文件用于管理使用pmr命令启动的进程例如:
[
{
"id": 1,
"name": "test",
"program": "python",
"args": ["-m", "test"]
},
{
"id": 2,
"name": "test2",
"program": "python",
"args": ["-m", "test2"]
}
]
修改listls命令添加参数 --system用于显示系统所有进程如果不添加该参数只显示在~/.pmr/dump.json中的进程
增加stop命令用于关闭指定进程例如pmr stop 1关闭~/.pmr/dump.json中id为1的进程或者pmr stop test关闭~/.pmr/dump.json中name为test的进程
6.
验证start命令成功执行notepad程序后相关信息是否写入dump.json并且执行stop命令能正常结束进程
7.继续修改pmr start命令使得以下几种方式均可成功
pmr start pmr_id // 启动存在于dump.json中的进程
pmr start name // 启动存在于dump.json中的进程
pmr start --config config.json // 从配置文件启动进程
pmr start program_name // 从程序启动进程
8.增加pmr stop命令用于关闭指定进程例如pmr stop 1关闭~/.pmr/dump.json中id为1的进程或者pmr stop test关闭~/.pmr/dump.json中name为test的进程
9.增加命令restart, 例如 pmr restart 1重启~/.pmr/dump.json中id为1的进程或者pmr restart test重启~/.pmr/dump.json中name为test的进程, pmr restart --config config.json, 从配置文件重启进程

1
src/base/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod process;

12
src/base/process.rs Normal file
View File

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PmrProcessInfo {
pub pmr_id: u32, // 自增ID
pub pid: u32, // 进程PID
pub name: String,
pub namespace: String,
pub status: String,
pub program: String,
pub args: Vec<String>,
}

57
src/commands/delete.rs Normal file
View File

@ -0,0 +1,57 @@
use super::super::config::dump::DumpConfig;
use super::list::list_processes;
use super::stop::stop_process;
pub fn delete_process(target: &str) {
let dump_config = DumpConfig::get_instance();
match dump_config.list_processes() {
Ok(processes) => {
let mut found = false;
// 首先尝试将target解析为pmr_id
if let Ok(pmr_id) = target.parse::<u32>() {
if let Some(process) = processes.iter().find(|p| p.pmr_id == pmr_id) {
// 如果进程还在运行,先停止它
if process.pid > 0 {
stop_process(target, false);
}
// 从配置中删除进程
dump_config
.delete_process(pmr_id)
.expect("Failed to delete process from dump file");
println!("Successfully deleted process '{}'", process.name);
found = true;
}
}
// 如果不是pmr_id尝试按name查找
if !found {
if let Some(process) = processes.iter().find(|p| p.name == target) {
// 如果进程还在运行,先停止它
if process.pid > 0 {
stop_process(&process.pmr_id.to_string(), false);
}
// 从配置中删除进程
dump_config
.delete_process(process.pmr_id)
.expect("Failed to delete process from dump file");
println!("Successfully deleted process '{}'", process.name);
found = true;
}
}
if found {
// 显示进程列表
println!("\nCurrent process list:");
list_processes(false);
} else {
eprintln!("No process found with id or name: {}", target);
}
}
Err(e) => {
eprintln!("Failed to read processes: {}", e);
}
}
}

109
src/commands/list.rs Normal file
View File

@ -0,0 +1,109 @@
use crate::config::dump::DumpConfig;
use serde::{Deserialize, Serialize};
use sysinfo::{System, Users};
use tabled::{Table, Tabled};
#[derive(Serialize, Deserialize)]
pub struct PmrProcess {
pub pmr_id: u32,
pub pid: u32,
pub name: String,
pub namespace: String,
pub program: String,
pub args: Vec<String>,
pub status: String,
}
#[derive(Tabled)]
struct ProcessInfo {
id: String,
name: String,
namespace: String,
version: String,
pid: String,
uptime: String,
restarts: String,
status: String,
cpu: String,
mem: String,
user: String,
}
pub fn read_pmr_processes() -> Vec<PmrProcess> {
let dump_config = DumpConfig::get_instance();
match dump_config.list_processes() {
Ok(processes) => processes
.into_iter()
.map(|p| PmrProcess {
pmr_id: p.pmr_id,
pid: p.pid,
name: p.name,
namespace: p.namespace,
program: p.program,
args: p.args,
status: p.status,
})
.collect(),
Err(e) => {
eprintln!("Failed to read processes: {}", e);
Vec::new()
}
}
}
pub fn list_processes(system: bool) {
if system {
let mut sys = System::new_all();
let users = Users::new_with_refreshed_list();
sys.refresh_all();
let processes: Vec<ProcessInfo> = sys
.processes()
.iter()
.map(|(&pid, process)| ProcessInfo {
id: "0".to_string(),
name: process.name().to_string_lossy().to_string(),
namespace: "default".to_string(),
version: "N/A".to_string(),
pid: pid.to_string(),
uptime: process.run_time().to_string(),
restarts: "0".to_string(),
status: process.status().to_string(),
cpu: format!("{:.1}%", process.cpu_usage()),
mem: format!("{:.1} MB", process.memory() as f64 / 1024.0 / 1024.0),
user: process
.user_id()
.and_then(|uid| users.get_user_by_id(uid))
.map_or("N/A".to_string(), |u| u.name().to_string()),
})
.collect();
let table = Table::new(processes).to_string();
println!("{}", table);
} else {
let pmr_processes = read_pmr_processes();
let processes: Vec<ProcessInfo> = pmr_processes
.iter()
.map(|p| ProcessInfo {
id: p.pmr_id.to_string(),
name: p.name.clone(),
namespace: "pmr".to_string(),
version: "N/A".to_string(),
pid: p.pid.to_string(),
uptime: "N/A".to_string(),
restarts: "0".to_string(),
status: if p.pid > 0 {
"running".to_string()
} else {
"stopped".to_string()
},
cpu: "N/A".to_string(),
mem: "N/A".to_string(),
user: "N/A".to_string(),
})
.collect();
let table = Table::new(processes).to_string();
println!("{}", table);
}
}

5
src/commands/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod delete;
pub mod list;
pub mod restart;
pub mod start;
pub mod stop;

64
src/commands/restart.rs Normal file
View File

@ -0,0 +1,64 @@
use super::super::base::process::PmrProcessInfo;
use super::super::config::dump::DumpConfig;
use super::list::list_processes;
use super::start::start_process;
use super::stop::stop_process;
use std::path::PathBuf;
pub fn restart_process(config: Option<PathBuf>, target: Option<String>, args: Vec<String>) {
let dump_config = DumpConfig::get_instance();
// 如果指定了target先检查是否是已存在的进程
if let Some(ref target_str) = target {
if let Ok(processes) = dump_config.list_processes() {
// 尝试将target解析为pmr_id
if let Ok(pmr_id) = target_str.parse::<u32>() {
if let Some(process) = processes.iter().find(|p| p.pmr_id == pmr_id) {
restart_existing_process(process);
return;
}
}
// 按名称查找进程
if let Some(process) = processes.iter().find(|p| p.name == *target_str) {
restart_existing_process(process);
return;
}
}
}
// 如果不是重启已存在的进程,就当作普通的启动处理
start_process(config, None, target, args);
}
fn restart_existing_process(process: &PmrProcessInfo) {
println!("正在重启进程 '{}'...", process.name);
// 先停止进程
stop_process(&process.pmr_id.to_string(), false);
// 重新启动进程
let mut cmd = std::process::Command::new(&process.program);
cmd.args(&process.args)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
println!("进程 '{}' 重启成功,新 PID: {}", process.name, pid);
let dump_config = DumpConfig::get_instance();
dump_config
.update_process_status(process.pmr_id, pid, "running".to_string())
.expect("无法更新进程状态");
// 显示进程列表
println!("\n当前进程列表:");
list_processes(false);
}
Err(e) => {
eprintln!("重启进程 '{}' 失败: {}", process.name, e);
}
}
}

157
src/commands/start.rs Normal file
View File

@ -0,0 +1,157 @@
use super::super::base::process::PmrProcessInfo;
use super::super::config::dump::DumpConfig;
use super::list::list_processes;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[derive(Deserialize, Serialize)]
struct Config {
name: String,
program: String,
args: Vec<String>,
}
pub fn start_process(config: Option<PathBuf>, name: Option<String>, target: Option<String>, args: Vec<String>) {
let dump_config = DumpConfig::get_instance();
// 如果指定了target先检查是否是已存在的进程
if let Some(ref target_str) = target {
if let Ok(processes) = dump_config.list_processes() {
// 尝试将target解析为pmr_id
if let Ok(pmr_id) = target_str.parse::<u32>() {
if let Some(process) = processes.iter().find(|p| p.pmr_id == pmr_id) {
start_existing_process(process);
return;
}
}
// 按名称查找进程
if let Some(process) = processes.iter().find(|p| p.name == *target_str) {
start_existing_process(process);
return;
}
}
}
// 检查是否已存在同名进程
let process_name = name.clone().unwrap_or_else(|| {
target.clone().unwrap_or_else(|| {
if let Some(ref config_path) = config {
let mut file = File::open(config_path).expect("Failed to open config file");
let mut contents = String::new();
file.read_to_string(&mut contents).expect("Failed to read config file");
let config: Config = serde_json::from_str(&contents).expect("Failed to parse config file");
config.name
} else {
"unnamed".to_string()
}
})
});
if let Ok(processes) = dump_config.list_processes() {
if let Some(_existing) = processes.iter().find(|p| p.name == process_name) {
println!("\n进程 '{}' 已经存在:", process_name);
list_processes(false);
return;
}
}
if let Some(config_path) = config {
// 从配置文件启动
let mut file = File::open(config_path).expect("无法打开配置文件");
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("无法读取配置文件");
let config: Config = serde_json::from_str(&contents).expect("无法解析配置文件");
let mut cmd = Command::new(&config.program);
cmd.args(&config.args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
println!("启动进程 '{}' PID: {}", process_name, pid);
dump_config
.add_process(
process_name,
"default".to_string(),
config.program,
pid,
"running".to_string(),
config.args,
)
.expect("无法将进程添加到配置文件");
}
Err(e) => {
eprintln!("启动进程失败: {}", e);
}
}
} else if let Some(target_program) = target {
// 直接启动程序
let mut cmd = Command::new(&target_program);
cmd.args(&args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
println!("启动进程 '{}' PID: {}", process_name, pid);
dump_config
.add_process(
process_name,
"default".to_string(),
target_program,
pid,
"running".to_string(),
args,
)
.expect("无法将进程添加到配置文件");
}
Err(e) => {
eprintln!("启动进程失败: {}", e);
}
}
} else {
eprintln!("错误: 必须指定 --config 或 target");
}
}
fn start_existing_process(process: &PmrProcessInfo) {
if process.status == "running" {
println!("进程 '{}' 已经在运行中PID: {}", process.name, process.pid);
return;
}
let mut cmd = Command::new(&process.program);
cmd.args(&process.args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
println!("启动进程 '{}' PID: {}", process.name, pid);
let dump_config = DumpConfig::get_instance();
dump_config
.update_process_status(process.pmr_id, pid, "running".to_string())
.expect("无法更新进程状态");
// 显示进程列表
println!("\n当前进程列表:");
list_processes(false);
}
Err(e) => {
eprintln!("启动进程 '{}' 失败: {}", process.name, e);
}
}
}

83
src/commands/stop.rs Normal file
View File

@ -0,0 +1,83 @@
use super::super::config::dump::DumpConfig;
use super::list::list_processes;
use std::process::Command;
pub fn stop_process(target: &str, show_list: bool) {
let dump_config = DumpConfig::get_instance();
match dump_config.list_processes() {
Ok(processes) => {
let mut found = false;
// 首先尝试将target解析为pmr_id
if let Ok(pmr_id) = target.parse::<u32>() {
if let Some(process) = processes.iter().find(|p| p.pmr_id == pmr_id) {
if process.pid > 0 {
// 在Windows上使用taskkill命令终止进程
let output = Command::new("taskkill")
.args(&["/PID", &process.pid.to_string(), "/F"])
.output()
.expect("无法执行taskkill命令");
if output.status.success() {
println!("已停止进程 '{}' (PID: {})", process.name, process.pid);
dump_config
.update_process_status(process.pmr_id, 0, "stopped".to_string())
.expect("无法更新进程状态");
found = true;
} else {
eprintln!(
"停止进程失败 '{}' (PID: {}): {}",
process.name,
process.pid,
String::from_utf8_lossy(&output.stderr)
);
}
} else {
println!("进程 '{}' 已经停止", process.name);
found = true;
}
}
}
// 如果不是pmr_id尝试按name查找
if !found {
if let Some(process) = processes.iter().find(|p| p.name == target) {
if process.pid > 0 {
// 在Windows上使用taskkill命令终止进程
let output = Command::new("taskkill")
.args(&["/PID", &process.pid.to_string(), "/F"])
.output()
.expect("无法执行taskkill命令");
if output.status.success() {
println!("已停止进程 '{}' (PID: {})", process.name, process.pid);
dump_config
.update_process_status(process.pmr_id, 0, "stopped".to_string())
.expect("无法更新进程状态");
} else {
eprintln!(
"停止进程失败 '{}' (PID: {}): {}",
process.name,
process.pid,
String::from_utf8_lossy(&output.stderr)
);
}
} else {
println!("进程 '{}' 已经停止", process.name);
}
} else {
eprintln!("未找到进程: {}", target);
}
}
// 根据show_list参数决定是否显示进程列表
if show_list {
println!("\n当前进程列表:");
list_processes(false);
}
}
Err(e) => {
eprintln!("读取进程列表失败: {}", e);
}
}
}

102
src/config/dump.rs Normal file
View File

@ -0,0 +1,102 @@
use super::super::base::process::PmrProcessInfo;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use std::{fs, io, path::PathBuf};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct DumpData {
processes: Vec<PmrProcessInfo>,
}
pub struct DumpConfig {
path: PathBuf,
data: Mutex<DumpData>,
}
static INSTANCE: OnceCell<DumpConfig> = OnceCell::new();
impl DumpConfig {
fn new() -> io::Result<Self> {
let home_dir = dirs::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?;
let config_dir = home_dir.join(".pmr");
if !config_dir.exists() {
fs::create_dir_all(&config_dir)?;
}
let dump_file = config_dir.join("dump.json");
let data = if dump_file.exists() {
let file_contents = fs::read_to_string(&dump_file)?;
serde_json::from_str(&file_contents)?
} else {
let initial_data = DumpData {
processes: Vec::new(),
};
fs::write(&dump_file, serde_json::to_string_pretty(&initial_data)?)?;
initial_data
};
Ok(Self {
path: dump_file,
data: Mutex::new(data),
})
}
pub fn get_instance() -> &'static DumpConfig {
INSTANCE.get_or_init(|| Self::new().expect("Failed to initialize DumpConfig"))
}
fn save_data(&self, data: &DumpData) -> io::Result<()> {
let json = serde_json::to_string_pretty(data)?;
fs::write(&self.path, json)
}
pub fn add_process(
&self,
name: String,
namespace: String,
program: String,
pid: u32,
status: String,
args: Vec<String>,
) -> io::Result<()> {
let mut data = self.data.lock().unwrap();
let new_id = data.processes.iter().map(|p| p.pmr_id).max().unwrap_or(0) + 1;
data.processes.push(PmrProcessInfo {
pmr_id: new_id,
name,
namespace,
pid,
status,
program,
args,
});
self.save_data(&data)
}
pub fn delete_process(&self, id: u32) -> io::Result<()> {
let mut data = self.data.lock().unwrap();
data.processes.retain(|p| p.pmr_id != id);
self.save_data(&data)
}
pub fn list_processes(&self) -> io::Result<Vec<PmrProcessInfo>> {
let data = self.data.lock().unwrap();
Ok(data.processes.clone())
}
pub fn update_process_status(&self, pmr_id: u32, pid: u32, status: String) -> io::Result<()> {
let mut data = self.data.lock().unwrap();
if let Some(process) = data.processes.iter_mut().find(|p| p.pmr_id == pmr_id) {
process.pid = pid;
process.status = status;
self.save_data(&data)
} else {
Ok(())
}
}
}

1
src/config/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod dump;

125
src/main.rs Normal file
View File

@ -0,0 +1,125 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
mod base;
mod commands;
mod config;
use commands::delete::delete_process;
use commands::list::list_processes;
use commands::restart::restart_process;
use commands::start::start_process;
use commands::stop::stop_process;
use config::dump::DumpConfig;
fn config_init() -> std::io::Result<()> {
// 使用DumpConfig初始化配置
let _ = DumpConfig::get_instance();
Ok(())
}
#[derive(Parser)]
#[command(name = "pmr")]
#[command(about = "Process Manager in Rust", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Start a process
Start {
/// Config file path
#[arg(short, long)]
config: Option<PathBuf>,
/// Process name
#[arg(short, long)]
name: Option<String>,
/// Target (can be pmr_id, name, or program to run)
target: Option<String>,
/// Arguments for the program
#[arg(last = true)]
args: Vec<String>,
},
/// List running processes
#[command(alias = "ls")]
List {
/// Show all system processes
#[arg(long)]
system: bool,
},
/// Delete a process
#[command(alias = "rm")]
Delete {
/// Process ID or name
target: String,
},
/// Stop a process
Stop {
/// Process ID or name
target: String,
},
/// Restart a process
Restart {
/// Config file path
#[arg(short, long)]
config: Option<PathBuf>,
/// Target (can be pmr_id, name, or program to run)
target: Option<String>,
/// Arguments for the program
#[arg(last = true)]
args: Vec<String>,
},
}
fn main() {
if let Err(e) = config_init() {
eprintln!("Failed to initialize .pmr directory: {}", e);
return;
}
let cli = Cli::parse();
match cli.command {
Commands::Start {
config,
name,
target,
args,
} => {
if config.is_none() && target.is_none() {
eprintln!("错误: 必须指定 --config 或 target");
return;
}
start_process(config.clone(), name.clone(), target.clone(), args);
}
Commands::List { system } => {
list_processes(system);
}
Commands::Delete { target } => {
delete_process(&target);
}
Commands::Stop { target } => {
stop_process(&target, true);
}
Commands::Restart {
config,
target,
args,
} => {
if config.is_none() && target.is_none() {
eprintln!("错误: 必须指定 --config 或 target");
return;
}
restart_process(config.clone(), target.clone(), args);
}
}
}