tauri_mcp/tools/
process.rs1use crate::{Result, TauriMcpError};
2use crossbeam_channel::{bounded, Receiver, Sender};
3use parking_lot::RwLock;
4use serde_json::Value;
5use std::collections::HashMap;
6use std::path::Path;
7use std::process::Stdio;
8use std::sync::Arc;
9use sysinfo::{System, Pid};
10use tokio::io::{AsyncBufReadExt, BufReader};
11use tokio::process::{Child, Command};
12use tokio::task::JoinHandle;
13use tracing::{debug, error, info, warn};
14use uuid::Uuid;
15
16pub struct ProcessManager {
17 processes: HashMap<String, ProcessInfo>,
18 system: Arc<RwLock<System>>,
19}
20
21struct ProcessInfo {
22 id: String,
23 child: Child,
24 pid: u32,
25 log_receiver: Receiver<String>,
26 log_handle: JoinHandle<()>,
27}
28
29impl ProcessManager {
30 pub fn new() -> Self {
31 Self {
32 processes: HashMap::new(),
33 system: Arc::new(RwLock::new(System::new_all())),
34 }
35 }
36
37 pub async fn launch_app(&mut self, app_path: &str, args: Vec<String>) -> Result<String> {
38 let path = Path::new(app_path);
39 if !path.exists() {
40 return Err(TauriMcpError::ProcessError(format!("App path does not exist: {}", app_path)));
41 }
42
43 info!("Launching Tauri app: {} with args: {:?}", app_path, args);
44
45 let mut cmd = Command::new(app_path);
46 cmd.args(args)
47 .stdout(Stdio::piped())
48 .stderr(Stdio::piped())
49 .stdin(Stdio::null());
50
51 let mut child = cmd.spawn()
52 .map_err(|e| TauriMcpError::ProcessError(format!("Failed to launch app: {}", e)))?;
53
54 let pid = child.id()
55 .ok_or_else(|| TauriMcpError::ProcessError("Failed to get process ID".to_string()))?;
56
57 let process_id = Uuid::new_v4().to_string();
58
59 let (log_sender, log_receiver) = bounded(1000);
60
61 let stdout = child.stdout.take()
62 .ok_or_else(|| TauriMcpError::ProcessError("Failed to capture stdout".to_string()))?;
63 let stderr = child.stderr.take()
64 .ok_or_else(|| TauriMcpError::ProcessError("Failed to capture stderr".to_string()))?;
65
66 let log_handle = tokio::spawn(Self::log_reader(stdout, stderr, log_sender));
67
68 let process_info = ProcessInfo {
69 id: process_id.clone(),
70 child,
71 pid,
72 log_receiver,
73 log_handle,
74 };
75
76 self.processes.insert(process_id.clone(), process_info);
77
78 info!("App launched successfully with process ID: {} (PID: {})", process_id, pid);
79
80 Ok(process_id)
81 }
82
83 pub async fn stop_app(&mut self, process_id: &str) -> Result<()> {
84 let mut process_info = self.processes.remove(process_id)
85 .ok_or_else(|| TauriMcpError::ProcessError(format!("Process not found: {}", process_id)))?;
86
87 info!("Stopping app with process ID: {}", process_id);
88
89 process_info.child.kill().await
90 .map_err(|e| TauriMcpError::ProcessError(format!("Failed to kill process: {}", e)))?;
91
92 process_info.log_handle.abort();
93
94 Ok(())
95 }
96
97 pub async fn get_app_logs(&self, process_id: &str, lines: Option<usize>) -> Result<Vec<String>> {
98 let process_info = self.processes.get(process_id)
99 .ok_or_else(|| TauriMcpError::ProcessError(format!("Process not found: {}", process_id)))?;
100
101 let mut logs = Vec::new();
102
103 while let Ok(log) = process_info.log_receiver.try_recv() {
104 logs.push(log);
105 }
106
107 if let Some(line_count) = lines {
108 let start = logs.len().saturating_sub(line_count);
109 logs = logs[start..].to_vec();
110 }
111
112 Ok(logs)
113 }
114
115 pub async fn monitor_resources(&self, process_id: &str) -> Result<Value> {
116 let process_info = self.processes.get(process_id)
117 .ok_or_else(|| TauriMcpError::ProcessError(format!("Process not found: {}", process_id)))?;
118
119 let mut system = self.system.write();
120 system.refresh_processes();
121
122 if let Some(process) = system.process(Pid::from_u32(process_info.pid)) {
123 Ok(serde_json::json!({
124 "cpu_usage": process.cpu_usage(),
125 "memory_usage": process.memory(),
126 "virtual_memory": process.virtual_memory(),
127 "disk_usage": {
128 "read_bytes": process.disk_usage().read_bytes,
129 "written_bytes": process.disk_usage().written_bytes,
130 },
131 "status": format!("{:?}", process.status()),
132 "start_time": process.start_time(),
133 "run_time": process.run_time(),
134 }))
135 } else {
136 Err(TauriMcpError::ProcessError("Failed to get process info".to_string()))
137 }
138 }
139
140 async fn log_reader(
141 stdout: tokio::process::ChildStdout,
142 stderr: tokio::process::ChildStderr,
143 sender: Sender<String>,
144 ) {
145 let stdout_reader = BufReader::new(stdout);
146 let stderr_reader = BufReader::new(stderr);
147
148 let stdout_sender = sender.clone();
149 let stderr_sender = sender;
150
151 let stdout_handle = tokio::spawn(async move {
152 let mut lines = stdout_reader.lines();
153 while let Ok(Some(line)) = lines.next_line().await {
154 let log_line = format!("[stdout] {}", line);
155 if stdout_sender.send(log_line).is_err() {
156 break;
157 }
158 }
159 });
160
161 let stderr_handle = tokio::spawn(async move {
162 let mut lines = stderr_reader.lines();
163 while let Ok(Some(line)) = lines.next_line().await {
164 let log_line = format!("[stderr] {}", line);
165 if stderr_sender.send(log_line).is_err() {
166 break;
167 }
168 }
169 });
170
171 let _ = tokio::join!(stdout_handle, stderr_handle);
172 }
173
174 pub fn get_running_processes(&self) -> Vec<String> {
175 self.processes.keys().cloned().collect()
176 }
177}