Skip to main content

iris_hub/services/
process_manager.rs

1//! # Process Manager Service
2//! 
3//! Este serviço é responsável pelo gerenciamento do ciclo de vida
4//! dos processos das aplicações configuradas.
5//! 
6//! ## Funcionalidades
7//! - Iniciar processos em terminais Windows
8//! - Parar processos em execução
9//! - Reiniciar processos
10//! - Monitorar estado dos processos
11//! - Limpeza automática de processos mortos
12
13use std::collections::{HashMap, HashSet};
14use std::fs;
15use std::process::{Child, Command};
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18
19#[cfg(windows)]
20use std::os::windows::process::CommandExt;
21
22use crate::core::{AppConfig, RunningProcess};
23
24/// Flags de criação do Windows para ocultar janelas de comando
25#[cfg(windows)]
26const CREATE_NO_WINDOW: u32 = 0x08000000;
27
28/// Gerenciador de Processos.
29/// 
30/// Controla o ciclo de vida de todos os processos das aplicações.
31/// Thread-safe através de Arc<Mutex>.
32/// 
33/// # Exemplo
34/// ```rust
35/// let manager = ProcessManager::new();
36/// manager.launch_app(&app_config);
37/// 
38/// if manager.is_running(&app_config.id) {
39///     manager.stop_app(&app_config.id);
40/// }
41/// ```
42pub struct ProcessManager {
43    /// Mapa de processos em execução (app_id -> RunningProcess)
44    running_apps: Arc<Mutex<HashMap<String, RunningProcess>>>,
45    
46    /// Conjunto de aplicações em processo de inicialização
47    loading_apps: Arc<Mutex<HashSet<String>>>,
48}
49
50impl ProcessManager {
51    /// Cria uma nova instância do gerenciador de processos
52    pub fn new() -> Self {
53        Self {
54            running_apps: Arc::new(Mutex::new(HashMap::new())),
55            loading_apps: Arc::new(Mutex::new(HashSet::new())),
56        }
57    }
58    
59    /// Retorna uma referência Arc para os processos em execução
60    pub fn running_apps(&self) -> Arc<Mutex<HashMap<String, RunningProcess>>> {
61        Arc::clone(&self.running_apps)
62    }
63    
64    /// Retorna uma referência Arc para as aplicações em loading
65    pub fn loading_apps(&self) -> Arc<Mutex<HashSet<String>>> {
66        Arc::clone(&self.loading_apps)
67    }
68    
69    /// Inicia uma aplicação em um novo terminal Windows.
70    /// 
71    /// Este método:
72    /// 1. Para qualquer processo anterior da mesma aplicação
73    /// 2. Marca a aplicação como "loading"
74    /// 3. Cria um arquivo batch temporário com os comandos
75    /// 4. Executa o batch em um novo terminal
76    /// 5. Registra o processo em execução
77    /// 
78    /// # Argumentos
79    /// * `app` - Configuração da aplicação a ser iniciada
80    pub fn launch_app(&self, app: &AppConfig) {
81        if app.commands.is_empty() {
82            return;
83        }
84
85        // Para o processo anterior se existir
86        self.stop_app(&app.id, Some(&app.name), Some(&app.commands));
87        
88        // Marca como loading
89        {
90            let mut loading = self.loading_apps.lock().unwrap();
91            loading.insert(app.id.clone());
92        }
93
94        // Clona os dados necessários para a thread
95        let app_clone = app.clone();
96        let running_apps = Arc::clone(&self.running_apps);
97        let loading_apps = Arc::clone(&self.loading_apps);
98
99        // Executa em uma thread separada para não bloquear a UI
100        std::thread::spawn(move || {
101            Self::launch_in_thread(app_clone, running_apps, loading_apps);
102        });
103    }
104    
105    /// Lógica de inicialização executada em thread separada
106    fn launch_in_thread(
107        app: AppConfig,
108        running_apps: Arc<Mutex<HashMap<String, RunningProcess>>>,
109        loading_apps: Arc<Mutex<HashSet<String>>>,
110    ) {
111        // Cria um arquivo batch temporário
112        let temp_dir = std::env::temp_dir();
113        let batch_file = temp_dir.join(format!("iris_{}.bat", app.id));
114        
115        // Monta o conteúdo do batch
116        let batch_content = Self::build_batch_content(&app);
117        
118        if fs::write(&batch_file, &batch_content).is_err() {
119            let mut loading = loading_apps.lock().unwrap();
120            loading.remove(&app.id);
121            return;
122        }
123
124        // Executa o batch
125        let child = Command::new("cmd")
126            .args(["/C", "start", "", &batch_file.to_string_lossy()])
127            .spawn();
128
129        // Aguarda um pouco para o console abrir
130        std::thread::sleep(Duration::from_millis(800));
131
132        if let Ok(child) = child {
133            // Tenta obter o PID do processo do console
134            let console_pid = Self::find_console_pid(&app.name);
135            
136            let mut running = running_apps.lock().unwrap();
137            running.insert(app.id.clone(), RunningProcess::new(child, console_pid));
138        }
139        
140        // Remove do loading
141        let mut loading = loading_apps.lock().unwrap();
142        loading.remove(&app.id);
143    }
144    
145    /// Constrói o conteúdo do arquivo batch para execução.
146    /// 
147    /// Trata comandos especiais como npm, yarn, cargo e scripts .bat.
148    /// Também detecta automaticamente inputs para scripts interativos.
149    fn build_batch_content(app: &AppConfig) -> String {
150        let mut batch_content = String::new();
151        batch_content.push_str("@echo off\n");
152        batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
153        
154        if !app.working_dir.is_empty() {
155            batch_content.push_str(&format!("cd /d \"{}\"\n", app.working_dir));
156        }
157        
158        let commands = &app.commands;
159        let mut i = 0;
160        while i < commands.len() {
161            let cmd = &commands[i];
162            let cmd_lower = cmd.to_lowercase();
163            
164            // Verifica se o próximo comando parece ser um input
165            let next_is_input = if i + 1 < commands.len() {
166                let next = &commands[i + 1];
167                next.chars().all(|c| c.is_numeric() || c == '.')
168                    || next.eq_ignore_ascii_case("s")
169                    || next.eq_ignore_ascii_case("n")
170                    || next.eq_ignore_ascii_case("y")
171            } else {
172                false
173            };
174            
175            // Se o comando atual é um .bat/.cmd e o próximo é input
176            if next_is_input && (cmd_lower.ends_with(".bat") || cmd_lower.ends_with(".cmd")) {
177                let input = &commands[i + 1];
178                let input_file = format!("iris_input_{}.txt", app.id);
179                batch_content.push_str(&format!("echo {}> %TEMP%\\{}\n", input, input_file));
180                batch_content.push_str(&format!("call {} < %TEMP%\\{}\n", cmd, input_file));
181                batch_content.push_str(&format!("del %TEMP%\\{} 2>nul\n", input_file));
182                i += 2;
183                batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
184                continue;
185            }
186            
187            // Comandos que precisam de "call"
188            let needs_call = cmd_lower.starts_with("npm ")
189                || cmd_lower.starts_with("yarn ")
190                || cmd_lower.starts_with("pnpm ")
191                || cmd_lower.starts_with("npx ")
192                || cmd_lower.starts_with("dotnet ")
193                || cmd_lower.starts_with("cargo ")
194                || cmd_lower.ends_with(".bat")
195                || cmd_lower.ends_with(".cmd");
196            
197            if needs_call {
198                batch_content.push_str(&format!("call {}\n", cmd));
199            } else {
200                batch_content.push_str(&format!("{}\n", cmd));
201            }
202            
203            batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
204            i += 1;
205        }
206        
207        batch_content.push_str(&format!("title [IRIS] {}\n", app.name));
208        batch_content.push_str("cmd /k\n");
209        
210        batch_content
211    }
212    
213    /// Encontra o PID do console Windows pelo título da janela.
214    fn find_console_pid(title: &str) -> Option<u32> {
215        let search_title = format!("[IRIS] {}", title);
216        
217        for _ in 0..5 {
218            // Busca pelo título exato
219            #[cfg(windows)]
220            let output = Command::new("powershell")
221                .args([
222                    "-NoProfile",
223                    "-Command",
224                    &format!(
225                        "Get-Process cmd -ErrorAction SilentlyContinue | Where-Object {{$_.MainWindowTitle -eq '{}'}} | Select-Object -First 1 -ExpandProperty Id",
226                        search_title
227                    ),
228                ])
229                .creation_flags(CREATE_NO_WINDOW)
230                .output()
231                .ok()?;
232            
233            #[cfg(not(windows))]
234            let output = Command::new("echo")
235                .arg("")
236                .output()
237                .ok()?;
238
239            let pid_str = String::from_utf8_lossy(&output.stdout);
240            if let Ok(pid) = pid_str.trim().parse() {
241                return Some(pid);
242            }
243            
244            // Tenta com -like se -eq não funcionou
245            #[cfg(windows)]
246            let output = Command::new("powershell")
247                .args([
248                    "-NoProfile",
249                    "-Command",
250                    &format!(
251                        "Get-Process cmd -ErrorAction SilentlyContinue | Where-Object {{$_.MainWindowTitle -like '*[IRIS]*{}*'}} | Select-Object -First 1 -ExpandProperty Id",
252                        title
253                    ),
254                ])
255                .creation_flags(CREATE_NO_WINDOW)
256                .output()
257                .ok()?;
258
259            #[cfg(not(windows))]
260            let output = Command::new("echo")
261                .arg("")
262                .output()
263                .ok()?;
264
265            let pid_str = String::from_utf8_lossy(&output.stdout);
266            if let Ok(pid) = pid_str.trim().parse() {
267                return Some(pid);
268            }
269            
270            std::thread::sleep(Duration::from_millis(300));
271        }
272        
273        None
274    }
275    
276    /// Para uma aplicação em execução.
277    /// 
278    /// Utiliza múltiplas estratégias para garantir que o processo seja terminado:
279    /// 1. Mata pelo título do comando
280    /// 2. Mata pelo título [IRIS]
281    /// 3. Mata pela árvore de processos (PID)
282    /// 4. Usa WMIC para matar pelo CommandLine
283    /// 
284    /// # Argumentos
285    /// * `app_id` - ID da aplicação a ser parada
286    /// * `app_name` - Nome opcional da aplicação (para busca por título)
287    /// * `commands` - Comandos opcionais (para busca por título)
288    pub fn stop_app(&self, app_id: &str, app_name: Option<&str>, commands: Option<&Vec<String>>) {
289        let mut running = self.running_apps.lock().unwrap();
290        
291        if let Some(mut process) = running.remove(app_id) {
292            // Estratégia 1: Mata pelo título do comando
293            if let Some(cmds) = commands {
294                for cmd in cmds {
295                    #[cfg(windows)]
296                    {
297                        let _ = Command::new("taskkill")
298                            .args(["/F", "/FI", &format!("WINDOWTITLE eq {}", cmd)])
299                            .creation_flags(CREATE_NO_WINDOW)
300                            .output();
301                        
302                        let _ = Command::new("taskkill")
303                            .args(["/F", "/FI", &format!("WINDOWTITLE eq {}*", cmd)])
304                            .creation_flags(CREATE_NO_WINDOW)
305                            .output();
306                    }
307                }
308            }
309
310            // Estratégia 2: Pelo título [IRIS] Nome
311            if let Some(name) = app_name {
312                #[cfg(windows)]
313                let _ = Command::new("taskkill")
314                    .args(["/F", "/FI", &format!("WINDOWTITLE eq [IRIS] {}", name)])
315                    .creation_flags(CREATE_NO_WINDOW)
316                    .output();
317            }
318
319            // Estratégia 3: Pela árvore de processos
320            if let Some(pid) = process.console_pid {
321                #[cfg(windows)]
322                let _ = Command::new("taskkill")
323                    .args(["/F", "/T", "/PID", &pid.to_string()])
324                    .creation_flags(CREATE_NO_WINDOW)
325                    .output();
326            }
327
328            // Estratégia 4: WMIC pelo CommandLine
329            #[cfg(windows)]
330            {
331                let batch_name = format!("iris_{}.bat", app_id);
332                let _ = Command::new("cmd")
333                    .args(["/C", &format!(
334                        "wmic process where \"CommandLine like '%{}%'\" call terminate 2>nul",
335                        batch_name
336                    )])
337                    .creation_flags(CREATE_NO_WINDOW)
338                    .output();
339            }
340
341            // Estratégia 5: Mata padrões comuns
342            #[cfg(windows)]
343            for pattern in ["npm*", "node*", "vite*", "yarn*", "pnpm*"] {
344                let _ = Command::new("taskkill")
345                    .args(["/F", "/FI", &format!("WINDOWTITLE eq {}", pattern)])
346                    .creation_flags(CREATE_NO_WINDOW)
347                    .output();
348            }
349            
350            // Mata o processo child diretamente
351            let _ = process.child.kill();
352        }
353    }
354    
355    /// Reinicia uma aplicação.
356    /// 
357    /// Para o processo atual e inicia novamente após um pequeno delay.
358    pub fn restart_app(&self, app: &AppConfig) {
359        self.stop_app(&app.id, Some(&app.name), Some(&app.commands));
360        std::thread::sleep(Duration::from_millis(200));
361        self.launch_app(app);
362    }
363    
364    /// Verifica se uma aplicação está em execução
365    pub fn is_running(&self, app_id: &str) -> bool {
366        let running = self.running_apps.lock().unwrap();
367        running.contains_key(app_id)
368    }
369    
370    /// Verifica se uma aplicação está em processo de inicialização
371    pub fn is_loading(&self, app_id: &str) -> bool {
372        let loading = self.loading_apps.lock().unwrap();
373        loading.contains(app_id)
374    }
375    
376    /// Retorna o número de aplicações em execução
377    pub fn running_count(&self) -> usize {
378        let running = self.running_apps.lock().unwrap();
379        running.len()
380    }
381    
382    /// Verifica se há alguma aplicação em loading
383    pub fn has_loading(&self) -> bool {
384        let loading = self.loading_apps.lock().unwrap();
385        !loading.is_empty()
386    }
387    
388    /// Verifica se há alguma aplicação em execução
389    pub fn has_running(&self) -> bool {
390        let running = self.running_apps.lock().unwrap();
391        !running.is_empty()
392    }
393    
394    /// Limpa processos que morreram do registro.
395    /// 
396    /// Verifica se os processos registrados ainda estão ativos
397    /// e remove os que foram encerrados.
398    pub fn cleanup_dead_processes(&self) {
399        let mut running = self.running_apps.lock().unwrap();
400        let mut to_remove = Vec::new();
401        
402        for (app_id, process) in running.iter() {
403            if let Some(pid) = process.console_pid {
404                #[cfg(windows)]
405                let output = Command::new("tasklist")
406                    .args(["/FI", &format!("PID eq {}", pid), "/NH"])
407                    .output();
408                
409                #[cfg(not(windows))]
410                let output = Command::new("ps")
411                    .args(["-p", &pid.to_string()])
412                    .output();
413                
414                if let Ok(output) = output {
415                    let output_str = String::from_utf8_lossy(&output.stdout);
416                    #[cfg(windows)]
417                    let is_dead = !output_str.to_lowercase().contains("cmd.exe");
418                    #[cfg(not(windows))]
419                    let is_dead = output_str.is_empty();
420                    
421                    if is_dead {
422                        to_remove.push(app_id.clone());
423                    }
424                }
425            }
426        }
427        
428        for app_id in to_remove {
429            running.remove(&app_id);
430        }
431    }
432}
433
434impl Default for ProcessManager {
435    fn default() -> Self {
436        Self::new()
437    }
438}