stynx_code_plugins/infrastructure/
subprocess_plugin.rs1use 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}