Skip to main content

stynx_code_plugins/infrastructure/
subprocess_plugin.rs

1use serde_json::Value;
2use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
3use tokio::process::{Child, ChildStdin, ChildStdout};
4
5use stynx_code_errors::{AppError, AppResult};
6
7pub struct SubprocessPlugin {
8    pub child: Child,
9    pub stdin: ChildStdin,
10    pub stdout: BufReader<ChildStdout>,
11}
12
13impl SubprocessPlugin {
14    pub fn new(mut child: Child) -> AppResult<Self> {
15        let stdin = child.stdin.take().ok_or_else(|| {
16            AppError::Internal(anyhow::anyhow!("Failed to capture plugin stdin"))
17        })?;
18        let stdout = child.stdout.take().ok_or_else(|| {
19            AppError::Internal(anyhow::anyhow!("Failed to capture plugin stdout"))
20        })?;
21
22        Ok(Self {
23            child,
24            stdin,
25            stdout: BufReader::new(stdout),
26        })
27    }
28
29    pub async fn send_request(&mut self, method: &str, params: Value) -> AppResult<Value> {
30        let request = serde_json::json!({
31            "jsonrpc": "2.0",
32            "id": 1,
33            "method": method,
34            "params": params,
35        });
36
37        let mut line = serde_json::to_string(&request)?;
38        line.push('\n');
39
40        self.stdin.write_all(line.as_bytes()).await.map_err(|e| {
41            AppError::Internal(anyhow::anyhow!("Failed to write to plugin stdin: {e}"))
42        })?;
43        self.stdin.flush().await.map_err(|e| {
44            AppError::Internal(anyhow::anyhow!("Failed to flush plugin stdin: {e}"))
45        })?;
46
47        let mut response_line = String::new();
48        let timeout = std::time::Duration::from_secs(30);
49        let read = tokio::time::timeout(timeout, self.stdout.read_line(&mut response_line))
50            .await
51            .map_err(|_| AppError::Internal(anyhow::anyhow!("plugin subprocess timed out after 30s")))?;
52        read.map_err(|e| {
53            AppError::Internal(anyhow::anyhow!("Failed to read plugin response: {e}"))
54        })?;
55
56        if response_line.is_empty() {
57            return Err(AppError::Internal(anyhow::anyhow!(
58                "Plugin closed stdout unexpectedly"
59            )));
60        }
61
62        let response: Value = serde_json::from_str(response_line.trim()).map_err(|e| {
63            AppError::Internal(anyhow::anyhow!("Invalid JSON response from plugin: {e}"))
64        })?;
65
66        if let Some(error) = response.get("error") {
67            return Err(AppError::Internal(anyhow::anyhow!(
68                "Plugin returned error: {error}"
69            )));
70        }
71
72        Ok(response["result"].clone())
73    }
74
75    pub fn is_alive(&mut self) -> bool {
76        matches!(self.child.try_wait(), Ok(None))
77    }
78}