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