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
12fn fire_and_forget<E: std::fmt::Debug>(result: Result<(), E>) {
17 if let Err(e) = result {
18 tracing::trace!(error = ?e, "fire-and-forget send failed");
19 }
20}
21
22pub async fn spawn_plugin_process(
37 process_id: u64,
38 command: String,
39 args: Vec<String>,
40 cwd: Option<String>,
41 sender: mpsc::Sender<AsyncMessage>,
42) {
43 let mut cmd = Command::new(&command);
45 cmd.args(&args);
46 cmd.stdout(Stdio::piped());
47 cmd.stderr(Stdio::piped());
48
49 #[cfg(windows)]
53 cmd.creation_flags(0x0800_0000);
54
55 if let Some(ref dir) = cwd {
57 cmd.current_dir(dir);
58 }
59
60 let mut child = match cmd.spawn() {
62 Ok(child) => child,
63 Err(e) => {
64 fire_and_forget(sender.send(AsyncMessage::ProcessOutput {
66 process_id,
67 stdout: String::new(),
68 stderr: format!("Failed to spawn process: {}", e),
69 exit_code: -1,
70 }));
71 return;
72 }
73 };
74
75 let stdout_handle = child.stdout.take();
77 let stderr_handle = child.stderr.take();
78
79 let stdout_future = async {
81 if let Some(stdout) = stdout_handle {
82 let reader = BufReader::new(stdout);
83 let mut lines = reader.lines();
84 let mut output = String::new();
85
86 while let Ok(Some(line)) = lines.next_line().await {
87 output.push_str(&line);
88 output.push('\n');
89 }
90 output
91 } else {
92 String::new()
93 }
94 };
95
96 let stderr_future = async {
98 if let Some(stderr) = stderr_handle {
99 let reader = BufReader::new(stderr);
100 let mut lines = reader.lines();
101 let mut output = String::new();
102
103 while let Ok(Some(line)) = lines.next_line().await {
104 output.push_str(&line);
105 output.push('\n');
106 }
107 output
108 } else {
109 String::new()
110 }
111 };
112
113 let (stdout, stderr) = tokio::join!(stdout_future, stderr_future);
115
116 let exit_code = match child.wait().await {
118 Ok(status) => status.code().unwrap_or(-1),
119 Err(_) => -1,
120 };
121
122 fire_and_forget(sender.send(AsyncMessage::ProcessOutput {
124 process_id,
125 stdout,
126 stderr,
127 exit_code,
128 }));
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[tokio::test]
136 async fn test_spawn_simple_command() {
137 let (sender, receiver) = mpsc::channel();
138
139 spawn_plugin_process(
140 1,
141 "echo".to_string(),
142 vec!["hello".to_string()],
143 None,
144 sender,
145 )
146 .await;
147
148 let msg = receiver.recv().unwrap();
149 match msg {
150 AsyncMessage::ProcessOutput {
151 process_id,
152 stdout,
153 stderr,
154 exit_code,
155 } => {
156 assert_eq!(process_id, 1);
157 assert!(stdout.contains("hello"));
158 assert_eq!(stderr, "");
159 assert_eq!(exit_code, 0);
160 }
161 _ => panic!("Expected PluginProcessOutput"),
162 }
163 }
164
165 #[tokio::test]
166 async fn test_spawn_with_args() {
167 let (sender, receiver) = mpsc::channel();
168
169 spawn_plugin_process(
170 2,
171 "printf".to_string(),
172 vec![
173 "%s %s".to_string(),
174 "hello".to_string(),
175 "world".to_string(),
176 ],
177 None,
178 sender,
179 )
180 .await;
181
182 let msg = receiver.recv().unwrap();
183 match msg {
184 AsyncMessage::ProcessOutput {
185 process_id,
186 stdout,
187 exit_code,
188 ..
189 } => {
190 assert_eq!(process_id, 2);
191 assert!(stdout.contains("hello world"));
192 assert_eq!(exit_code, 0);
193 }
194 _ => panic!("Expected PluginProcessOutput"),
195 }
196 }
197
198 #[tokio::test]
199 async fn test_spawn_nonexistent_command() {
200 let (sender, receiver) = mpsc::channel();
201
202 spawn_plugin_process(
203 3,
204 "this_command_does_not_exist_12345".to_string(),
205 vec![],
206 None,
207 sender,
208 )
209 .await;
210
211 let msg = receiver.recv().unwrap();
212 match msg {
213 AsyncMessage::ProcessOutput {
214 process_id,
215 stdout,
216 stderr,
217 exit_code,
218 } => {
219 assert_eq!(process_id, 3);
220 assert_eq!(stdout, "");
221 assert!(stderr.contains("Failed to spawn"));
222 assert_eq!(exit_code, -1);
223 }
224 _ => panic!("Expected PluginProcessOutput"),
225 }
226 }
227
228 #[tokio::test]
229 async fn test_spawn_failing_command() {
230 let (sender, receiver) = mpsc::channel();
231
232 spawn_plugin_process(
234 4,
235 "sh".to_string(),
236 vec!["-c".to_string(), "exit 42".to_string()],
237 None,
238 sender,
239 )
240 .await;
241
242 let msg = receiver.recv().unwrap();
243 match msg {
244 AsyncMessage::ProcessOutput {
245 process_id,
246 exit_code,
247 ..
248 } => {
249 assert_eq!(process_id, 4);
250 assert_eq!(exit_code, 42);
251 }
252 _ => panic!("Expected PluginProcessOutput"),
253 }
254 }
255}