use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::fs;
use std::process::{Command, Stdio};
use super::output;
const PROCFILE: &str = ".oxidite_procs.json";
#[derive(Debug, Serialize, Deserialize)]
struct ProcessInfo {
id: String,
name: String,
pid: u32,
status: String,
started_at: String,
cwd: String,
command: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ProcessList {
processes: Vec<ProcessInfo>,
}
impl ProcessList {
fn load() -> Self {
if let Ok(content) = fs::read_to_string(PROCFILE) {
if let Ok(list) = serde_json::from_str(&content) {
return list;
}
}
ProcessList {
processes: Vec::new(),
}
}
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let content = serde_json::to_string_pretty(self)?;
fs::write(PROCFILE, content)?;
Ok(())
}
fn add(&mut self, proc: ProcessInfo) {
self.processes.push(proc);
}
fn remove(&mut self, id: &str) -> bool {
let len = self.processes.len();
self.processes.retain(|p| p.id != id);
self.processes.len() < len
}
#[allow(dead_code)]
fn find(&self, id: &str) -> Option<&ProcessInfo> {
self.processes.iter().find(|p| p.id == id)
}
}
pub fn start_process(name: Option<String>, release: bool) -> Result<(), Box<dyn std::error::Error>> {
output::header("Starting Oxidite process");
let proc_name = name.unwrap_or_else(|| "oxidite-app".to_string());
let proc_id = format!("{}-{}", proc_name, std::process::id());
let mut procs = ProcessList::load();
if procs.processes.iter().any(|p| p.name == proc_name) {
output::warning(&format!("Process '{}' already exists, stopping it first", proc_name));
stop_process_by_name(&proc_name)?;
procs = ProcessList::load(); }
output::step(&format!("Starting process '{}'", proc_name));
output::debug(&format!("Process ID: {}", proc_id));
output::debug(&format!("Release mode: {}", release));
let child = Command::new("cargo")
.arg("run")
.args(if release { vec!["--release"] } else { vec![] })
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let pid = child.id();
let started_at = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let cwd = std::env::current_dir()?.display().to_string();
output::info(&format!("Process started with PID {}", pid));
procs.add(ProcessInfo {
id: proc_id.clone(),
name: proc_name.clone(),
pid,
status: "online".to_string(),
started_at,
cwd,
command: format!("cargo run{}", if release { " --release" } else { "" }),
});
procs.save()?;
output::success(&format!("Process '{}' is now running", proc_name));
output::debug(&format!("Process file: {}", PROCFILE));
Ok(())
}
pub fn stop_process(identifier: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
output::header("Stopping Oxidite process");
let procs = ProcessList::load();
if procs.processes.is_empty() {
output::warning("No processes found");
return Ok(());
}
let target = if let Some(id) = identifier {
procs.processes.iter().find(|p| p.name == id || p.id == id)
.ok_or_else(|| format!("Process '{}' not found", id))?
} else {
output::info("Stopping all processes");
for proc_info in &procs.processes {
stop_single_process(proc_info)?;
}
return Ok(());
};
stop_single_process(target)?;
Ok(())
}
fn stop_single_process(proc_info: &ProcessInfo) -> Result<(), Box<dyn std::error::Error>> {
output::step(&format!("Stopping process '{}' (PID: {})", proc_info.name, proc_info.pid));
#[cfg(unix)]
{
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
let _ = kill(Pid::from_raw(proc_info.pid as i32), Signal::SIGTERM);
}
#[cfg(windows)]
{
Command::new("taskkill")
.args(&["/F", "/PID", &proc_info.pid.to_string()])
.output()?;
}
let mut procs = ProcessList::load();
procs.remove(&proc_info.id);
procs.save()?;
output::success(&format!("Process '{}' stopped", proc_info.name));
Ok(())
}
fn stop_process_by_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
let procs = ProcessList::load();
if let Some(proc_info) = procs.processes.iter().find(|p| p.name == name) {
stop_single_process(proc_info)?;
}
Ok(())
}
pub fn restart_process(identifier: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
output::header("Restarting Oxidite process");
let procs = ProcessList::load();
if procs.processes.is_empty() {
output::warning("No processes found to restart");
return Ok(());
}
for proc_info in &procs.processes {
if let Some(ref id) = identifier {
if proc_info.name != *id && proc_info.id != *id {
continue;
}
}
output::step(&format!("Restarting '{}'", proc_info.name));
let _ = stop_single_process(proc_info);
let is_release = proc_info.command.contains("--release");
let _ = start_process(Some(proc_info.name.clone()), is_release);
}
Ok(())
}
pub fn list_processes() -> Result<(), Box<dyn std::error::Error>> {
output::header("Oxidite Process List");
let procs = ProcessList::load();
if procs.processes.is_empty() {
output::info("No processes running");
output::info("Start a process with: oxidite pm2 start");
return Ok(());
}
println!("{}", format!("{:<20} {:<8} {:<10} {:<22}", "Name", "PID", "Status", "Started").cyan().bold());
println!("{}", "─".repeat(62).dimmed());
for proc_info in &procs.processes {
let status_color = match proc_info.status.as_str() {
"online" => proc_info.status.green(),
"stopped" => proc_info.status.red(),
"error" => proc_info.status.red().bold(),
_ => proc_info.status.yellow(),
};
println!(
"{:<20} {:<8} {:<10} {:<22}",
proc_info.name.bold(),
proc_info.pid,
status_color,
proc_info.started_at
);
}
println!("\nTotal: {} process{}", procs.processes.len(), if procs.processes.len() == 1 { "" } else { "s" });
Ok(())
}
pub fn show_process(identifier: &str) -> Result<(), Box<dyn std::error::Error>> {
let procs = ProcessList::load();
let proc_info = procs.processes.iter().find(|p| p.name == identifier || p.id == identifier)
.ok_or_else(|| format!("Process '{}' not found", identifier))?;
output::header(&format!("Process: {}", proc_info.name));
println!(" {:<15} {}", "ID:".bold(), proc_info.id);
println!(" {:<15} {}", "Name:".bold(), proc_info.name);
println!(" {:<15} {}", "PID:".bold(), proc_info.pid);
println!(" {:<15} {}", "Status:".bold(), proc_info.status);
println!(" {:<15} {}", "Started:".bold(), proc_info.started_at);
println!(" {:<15} {}", "Working Dir:".bold(), proc_info.cwd);
println!(" {:<15} {}", "Command:".bold(), proc_info.command);
Ok(())
}
pub fn monitor_processes() -> Result<(), Box<dyn std::error::Error>> {
output::header("Monitoring Oxidite Processes (Ctrl+C to exit)");
loop {
let procs = ProcessList::load();
if procs.processes.is_empty() {
output::info("No processes running");
} else {
for proc_info in &procs.processes {
let is_alive = is_process_alive(proc_info.pid);
let status_symbol = if is_alive {
"●".green()
} else {
"●".red()
};
println!("{} {} (PID: {})", status_symbol, proc_info.name, proc_info.pid);
}
}
println!();
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
kill(Pid::from_raw(pid as i32), Signal::SIGCONT).is_ok()
}
#[cfg(windows)]
{
Command::new("tasklist")
.args(&["/FI", &format!("PID eq {}", pid)])
.output()
.map(|output| {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.contains(&pid.to_string())
})
.unwrap_or(false)
}
}