Skip to main content

fresh_plugin_runtime/
process.rs

1//! Plugin Process Spawning: Async process execution for plugins
2//!
3//! This module enables plugins to spawn external processes asynchronously,
4//! capturing stdout/stderr and notifying via callbacks when complete.
5
6use fresh_core::api::PluginAsyncMessage as AsyncMessage;
7use std::process::Stdio;
8use std::sync::mpsc;
9use tokio::io::{AsyncBufReadExt, BufReader};
10use tokio::process::Command;
11
12/// Spawn an external process for a plugin
13///
14/// This function:
15/// 1. Spawns the process asynchronously
16/// 2. Captures all stdout and stderr
17/// 3. Waits for process completion
18/// 4. Sends results back via AsyncBridge with process_id for callback matching
19///
20/// # Arguments
21/// * `process_id` - Unique ID to match with callback
22/// * `command` - Command to execute (e.g., "git")
23/// * `args` - Command arguments
24/// * `cwd` - Optional working directory
25/// * `sender` - Channel to send results back to main loop
26pub 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    // Build the command
34    let mut cmd = Command::new(&command);
35    cmd.args(&args);
36    cmd.stdout(Stdio::piped());
37    cmd.stderr(Stdio::piped());
38
39    // Set working directory if provided
40    if let Some(ref dir) = cwd {
41        cmd.current_dir(dir);
42    }
43
44    // Spawn the process
45    let mut child = match cmd.spawn() {
46        Ok(child) => child,
47        Err(e) => {
48            // Failed to spawn - send error result
49            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    // Capture stdout and stderr
60    let stdout_handle = child.stdout.take();
61    let stderr_handle = child.stderr.take();
62
63    // Read stdout
64    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    // Read stderr
81    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    // Wait for both outputs concurrently
98    let (stdout, stderr) = tokio::join!(stdout_future, stderr_future);
99
100    // Wait for process to complete
101    let exit_code = match child.wait().await {
102        Ok(status) => status.code().unwrap_or(-1),
103        Err(_) => -1,
104    };
105
106    // Send results back to main loop
107    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        // Use a command that will fail
217        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}