fresh_plugin_runtime/
process.rs1use fresh_core::api::PluginAsyncMessage as AsyncMessage;
7use std::process::Stdio;
8use std::sync::mpsc;
9use tokio::io::{AsyncBufReadExt, BufReader};
10use tokio::process::Command;
11
12pub async fn spawn_plugin_process(
27 process_id: u64,
28 command: String,
29 args: Vec<String>,
30 cwd: Option<String>,
31 sender: mpsc::Sender<AsyncMessage>,
32) {
33 let mut cmd = Command::new(&command);
35 cmd.args(&args);
36 cmd.stdout(Stdio::piped());
37 cmd.stderr(Stdio::piped());
38
39 if let Some(ref dir) = cwd {
41 cmd.current_dir(dir);
42 }
43
44 let mut child = match cmd.spawn() {
46 Ok(child) => child,
47 Err(e) => {
48 let _ = sender.send(AsyncMessage::ProcessOutput {
50 process_id,
51 stdout: String::new(),
52 stderr: format!("Failed to spawn process: {}", e),
53 exit_code: -1,
54 });
55 return;
56 }
57 };
58
59 let stdout_handle = child.stdout.take();
61 let stderr_handle = child.stderr.take();
62
63 let stdout_future = async {
65 if let Some(stdout) = stdout_handle {
66 let reader = BufReader::new(stdout);
67 let mut lines = reader.lines();
68 let mut output = String::new();
69
70 while let Ok(Some(line)) = lines.next_line().await {
71 output.push_str(&line);
72 output.push('\n');
73 }
74 output
75 } else {
76 String::new()
77 }
78 };
79
80 let stderr_future = async {
82 if let Some(stderr) = stderr_handle {
83 let reader = BufReader::new(stderr);
84 let mut lines = reader.lines();
85 let mut output = String::new();
86
87 while let Ok(Some(line)) = lines.next_line().await {
88 output.push_str(&line);
89 output.push('\n');
90 }
91 output
92 } else {
93 String::new()
94 }
95 };
96
97 let (stdout, stderr) = tokio::join!(stdout_future, stderr_future);
99
100 let exit_code = match child.wait().await {
102 Ok(status) => status.code().unwrap_or(-1),
103 Err(_) => -1,
104 };
105
106 let _ = sender.send(AsyncMessage::ProcessOutput {
108 process_id,
109 stdout,
110 stderr,
111 exit_code,
112 });
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[tokio::test]
120 async fn test_spawn_simple_command() {
121 let (sender, receiver) = mpsc::channel();
122
123 spawn_plugin_process(
124 1,
125 "echo".to_string(),
126 vec!["hello".to_string()],
127 None,
128 sender,
129 )
130 .await;
131
132 let msg = receiver.recv().unwrap();
133 match msg {
134 AsyncMessage::ProcessOutput {
135 process_id,
136 stdout,
137 stderr,
138 exit_code,
139 } => {
140 assert_eq!(process_id, 1);
141 assert!(stdout.contains("hello"));
142 assert_eq!(stderr, "");
143 assert_eq!(exit_code, 0);
144 }
145 _ => panic!("Expected PluginProcessOutput"),
146 }
147 }
148
149 #[tokio::test]
150 async fn test_spawn_with_args() {
151 let (sender, receiver) = mpsc::channel();
152
153 spawn_plugin_process(
154 2,
155 "printf".to_string(),
156 vec![
157 "%s %s".to_string(),
158 "hello".to_string(),
159 "world".to_string(),
160 ],
161 None,
162 sender,
163 )
164 .await;
165
166 let msg = receiver.recv().unwrap();
167 match msg {
168 AsyncMessage::ProcessOutput {
169 process_id,
170 stdout,
171 exit_code,
172 ..
173 } => {
174 assert_eq!(process_id, 2);
175 assert!(stdout.contains("hello world"));
176 assert_eq!(exit_code, 0);
177 }
178 _ => panic!("Expected PluginProcessOutput"),
179 }
180 }
181
182 #[tokio::test]
183 async fn test_spawn_nonexistent_command() {
184 let (sender, receiver) = mpsc::channel();
185
186 spawn_plugin_process(
187 3,
188 "this_command_does_not_exist_12345".to_string(),
189 vec![],
190 None,
191 sender,
192 )
193 .await;
194
195 let msg = receiver.recv().unwrap();
196 match msg {
197 AsyncMessage::ProcessOutput {
198 process_id,
199 stdout,
200 stderr,
201 exit_code,
202 } => {
203 assert_eq!(process_id, 3);
204 assert_eq!(stdout, "");
205 assert!(stderr.contains("Failed to spawn"));
206 assert_eq!(exit_code, -1);
207 }
208 _ => panic!("Expected PluginProcessOutput"),
209 }
210 }
211
212 #[tokio::test]
213 async fn test_spawn_failing_command() {
214 let (sender, receiver) = mpsc::channel();
215
216 spawn_plugin_process(
218 4,
219 "sh".to_string(),
220 vec!["-c".to_string(), "exit 42".to_string()],
221 None,
222 sender,
223 )
224 .await;
225
226 let msg = receiver.recv().unwrap();
227 match msg {
228 AsyncMessage::ProcessOutput {
229 process_id,
230 exit_code,
231 ..
232 } => {
233 assert_eq!(process_id, 4);
234 assert_eq!(exit_code, 42);
235 }
236 _ => panic!("Expected PluginProcessOutput"),
237 }
238 }
239}