use std::collections::{HashMap, HashSet};
use std::fs;
use std::process::{Child, Command};
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use crate::core::{AppConfig, RunningProcess};
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub struct ProcessManager {
running_apps: Arc<Mutex<HashMap<String, RunningProcess>>>,
loading_apps: Arc<Mutex<HashSet<String>>>,
}
impl ProcessManager {
pub fn new() -> Self {
Self {
running_apps: Arc::new(Mutex::new(HashMap::new())),
loading_apps: Arc::new(Mutex::new(HashSet::new())),
}
}
pub fn running_apps(&self) -> Arc<Mutex<HashMap<String, RunningProcess>>> {
Arc::clone(&self.running_apps)
}
pub fn loading_apps(&self) -> Arc<Mutex<HashSet<String>>> {
Arc::clone(&self.loading_apps)
}
pub fn launch_app(&self, app: &AppConfig) {
if app.commands.is_empty() {
return;
}
self.stop_app(&app.id, Some(&app.name), Some(&app.commands));
{
let mut loading = self.loading_apps.lock().unwrap();
loading.insert(app.id.clone());
}
let app_clone = app.clone();
let running_apps = Arc::clone(&self.running_apps);
let loading_apps = Arc::clone(&self.loading_apps);
std::thread::spawn(move || {
Self::launch_in_thread(app_clone, running_apps, loading_apps);
});
}
fn launch_in_thread(
app: AppConfig,
running_apps: Arc<Mutex<HashMap<String, RunningProcess>>>,
loading_apps: Arc<Mutex<HashSet<String>>>,
) {
let temp_dir = std::env::temp_dir();
let batch_file = temp_dir.join(format!("iris_{}.bat", app.id));
let batch_content = Self::build_batch_content(&app);
if fs::write(&batch_file, &batch_content).is_err() {
let mut loading = loading_apps.lock().unwrap();
loading.remove(&app.id);
return;
}
let child = Command::new("cmd")
.args(["/C", "start", "", &batch_file.to_string_lossy()])
.spawn();
std::thread::sleep(Duration::from_millis(800));
if let Ok(child) = child {
let console_pid = Self::find_console_pid(&app.name);
let mut running = running_apps.lock().unwrap();
running.insert(app.id.clone(), RunningProcess::new(child, console_pid));
}
let mut loading = loading_apps.lock().unwrap();
loading.remove(&app.id);
}
fn build_batch_content(app: &AppConfig) -> String {
let mut batch_content = String::new();
batch_content.push_str("@echo off\n");
batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
if !app.working_dir.is_empty() {
batch_content.push_str(&format!("cd /d \"{}\"\n", app.working_dir));
}
let commands = &app.commands;
let mut i = 0;
while i < commands.len() {
let cmd = &commands[i];
let cmd_lower = cmd.to_lowercase();
let next_is_input = if i + 1 < commands.len() {
let next = &commands[i + 1];
next.chars().all(|c| c.is_numeric() || c == '.')
|| next.eq_ignore_ascii_case("s")
|| next.eq_ignore_ascii_case("n")
|| next.eq_ignore_ascii_case("y")
} else {
false
};
if next_is_input && (cmd_lower.ends_with(".bat") || cmd_lower.ends_with(".cmd")) {
let input = &commands[i + 1];
let input_file = format!("iris_input_{}.txt", app.id);
batch_content.push_str(&format!("echo {}> %TEMP%\\{}\n", input, input_file));
batch_content.push_str(&format!("call {} < %TEMP%\\{}\n", cmd, input_file));
batch_content.push_str(&format!("del %TEMP%\\{} 2>nul\n", input_file));
i += 2;
batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
continue;
}
let needs_call = cmd_lower.starts_with("npm ")
|| cmd_lower.starts_with("yarn ")
|| cmd_lower.starts_with("pnpm ")
|| cmd_lower.starts_with("npx ")
|| cmd_lower.starts_with("dotnet ")
|| cmd_lower.starts_with("cargo ")
|| cmd_lower.ends_with(".bat")
|| cmd_lower.ends_with(".cmd");
if needs_call {
batch_content.push_str(&format!("call {}\n", cmd));
} else {
batch_content.push_str(&format!("{}\n", cmd));
}
batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
i += 1;
}
batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
batch_content.push_str("cmd /k\n");
batch_content
}
fn find_console_pid(title: &str) -> Option<u32> {
let search_title = format!("[IRIS] {}", title);
for _ in 0..5 {
#[cfg(windows)]
let output = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Get-Process cmd -ErrorAction SilentlyContinue | Where-Object {{$_.MainWindowTitle -eq '{}'}} | Select-Object -First 1 -ExpandProperty Id",
search_title
),
])
.creation_flags(CREATE_NO_WINDOW)
.output()
.ok()?;
#[cfg(not(windows))]
let output = Command::new("echo")
.arg("")
.output()
.ok()?;
let pid_str = String::from_utf8_lossy(&output.stdout);
if let Ok(pid) = pid_str.trim().parse() {
return Some(pid);
}
#[cfg(windows)]
let output = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Get-Process cmd -ErrorAction SilentlyContinue | Where-Object {{$_.MainWindowTitle -like '*[IRIS]*{}*'}} | Select-Object -First 1 -ExpandProperty Id",
title
),
])
.creation_flags(CREATE_NO_WINDOW)
.output()
.ok()?;
#[cfg(not(windows))]
let output = Command::new("echo")
.arg("")
.output()
.ok()?;
let pid_str = String::from_utf8_lossy(&output.stdout);
if let Ok(pid) = pid_str.trim().parse() {
return Some(pid);
}
std::thread::sleep(Duration::from_millis(300));
}
None
}
pub fn stop_app(&self, app_id: &str, app_name: Option<&str>, commands: Option<&Vec<String>>) {
let mut running = self.running_apps.lock().unwrap();
if let Some(mut process) = running.remove(app_id) {
if let Some(cmds) = commands {
for cmd in cmds {
#[cfg(windows)]
{
let _ = Command::new("taskkill")
.args(["/F", "/FI", &format!("WINDOWTITLE eq {}", cmd)])
.creation_flags(CREATE_NO_WINDOW)
.output();
let _ = Command::new("taskkill")
.args(["/F", "/FI", &format!("WINDOWTITLE eq {}*", cmd)])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
}
}
if let Some(name) = app_name {
#[cfg(windows)]
let _ = Command::new("taskkill")
.args(["/F", "/FI", &format!("WINDOWTITLE eq [IRIS] {}", name)])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
if let Some(pid) = process.console_pid {
#[cfg(windows)]
let _ = Command::new("taskkill")
.args(["/F", "/T", "/PID", &pid.to_string()])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
#[cfg(windows)]
{
let batch_name = format!("iris_{}.bat", app_id);
let _ = Command::new("cmd")
.args(["/C", &format!(
"wmic process where \"CommandLine like '%{}%'\" call terminate 2>nul",
batch_name
)])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
#[cfg(windows)]
for pattern in ["npm*", "node*", "vite*", "yarn*", "pnpm*"] {
let _ = Command::new("taskkill")
.args(["/F", "/FI", &format!("WINDOWTITLE eq {}", pattern)])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
let _ = process.child.kill();
}
}
pub fn restart_app(&self, app: &AppConfig) {
self.stop_app(&app.id, Some(&app.name), Some(&app.commands));
std::thread::sleep(Duration::from_millis(200));
self.launch_app(app);
}
pub fn is_running(&self, app_id: &str) -> bool {
let running = self.running_apps.lock().unwrap();
running.contains_key(app_id)
}
pub fn is_loading(&self, app_id: &str) -> bool {
let loading = self.loading_apps.lock().unwrap();
loading.contains(app_id)
}
pub fn running_count(&self) -> usize {
let running = self.running_apps.lock().unwrap();
running.len()
}
pub fn has_loading(&self) -> bool {
let loading = self.loading_apps.lock().unwrap();
!loading.is_empty()
}
pub fn has_running(&self) -> bool {
let running = self.running_apps.lock().unwrap();
!running.is_empty()
}
pub fn cleanup_dead_processes(&self) {
let mut running = self.running_apps.lock().unwrap();
let mut to_remove = Vec::new();
for (app_id, process) in running.iter() {
if let Some(pid) = process.console_pid {
#[cfg(windows)]
let output = Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid), "/NH"])
.output();
#[cfg(not(windows))]
let output = Command::new("ps")
.args(["-p", &pid.to_string()])
.output();
if let Ok(output) = output {
let output_str = String::from_utf8_lossy(&output.stdout);
#[cfg(windows)]
let is_dead = !output_str.to_lowercase().contains("cmd.exe");
#[cfg(not(windows))]
let is_dead = output_str.is_empty();
if is_dead {
to_remove.push(app_id.clone());
}
}
}
}
for app_id in to_remove {
running.remove(&app_id);
}
}
}
impl Default for ProcessManager {
fn default() -> Self {
Self::new()
}
}