1use std::ffi::OsStr;
13use std::path::Path;
14use std::process::Stdio;
15use std::time::Duration;
16
17use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
18use tokio::process::{Child, ChildStdin, Command};
19use tokio::sync::mpsc;
20
21const MAX_SIDECAR_LINE_BYTES: u64 = 1024 * 1024; use super::protocol::{InsertTextMode, SidecarCommand, SidecarFrame, SIDECAR_PROTOCOL_VERSION};
26
27const EVENT_CHANNEL_CAPACITY: usize = 64;
28const COMMAND_WRITE_TIMEOUT: Duration = Duration::from_secs(2);
30
31#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum SidecarLifecycleEvent {
36 Ready {
38 protocol_version: u16,
39 extension: String,
40 capabilities: Vec<String>,
41 },
42 StateChanged {
44 state: String,
45 label: Option<String>,
46 },
47 InsertText { text: String, mode: InsertTextMode },
49 Error(String),
51 Exited,
53}
54
55#[derive(Debug, thiserror::Error)]
57pub enum SidecarError {
58 #[error("failed to spawn sidecar {bin}: {source}")]
59 Spawn {
60 bin: String,
61 #[source]
62 source: std::io::Error,
63 },
64 #[error("sidecar stdin/stdout was not captured")]
65 PipesUnavailable,
66 #[error("sidecar IO error: {0}")]
67 Io(#[from] std::io::Error),
68 #[error("sidecar command input is closed")]
70 AlreadyShutDown,
71 #[error("failed to encode sidecar command: {0}")]
72 Encode(#[from] serde_json::Error),
73 #[error("sidecar protocol error: {0}")]
74 Protocol(String),
75}
76
77pub struct SidecarManager {
82 hello_capabilities: Vec<String>,
83 child: Option<Child>,
84 stdin: Option<ChildStdin>,
85 rx: mpsc::Receiver<SidecarLifecycleEvent>,
86 reader_handle: Option<tokio::task::JoinHandle<()>>,
87 stderr_handle: Option<tokio::task::JoinHandle<()>>,
88}
89
90impl SidecarManager {
91 pub async fn spawn(
98 bin: &Path,
99 args: &[String],
100 config: serde_json::Value,
101 ) -> Result<Self, SidecarError> {
102 let mut command = Command::new(bin);
103 command
104 .args(args.iter().map(OsStr::new))
105 .stdin(Stdio::piped())
106 .stdout(Stdio::piped())
107 .stderr(Stdio::piped())
108 .kill_on_drop(true);
109
110 let mut child = command.spawn().map_err(|source| SidecarError::Spawn {
111 bin: bin.display().to_string(),
112 source,
113 })?;
114
115 let stdin = child.stdin.take().ok_or(SidecarError::PipesUnavailable)?;
116 let stdout = child.stdout.take().ok_or(SidecarError::PipesUnavailable)?;
117 let stderr = child.stderr.take();
118
119 let (tx, rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
120
121 let event_tx = tx.clone();
123 let reader_handle = tokio::spawn(async move {
124 let mut reader = BufReader::new(stdout);
125 let mut line = String::new();
126 loop {
127 line.clear();
128 let mut limited = (&mut reader).take(MAX_SIDECAR_LINE_BYTES + 1);
129 match tokio::io::AsyncBufReadExt::read_line(&mut limited, &mut line).await {
130 Ok(0) => break, Ok(_) => {}
132 Err(_) => break,
133 }
134 if line.len() as u64 > MAX_SIDECAR_LINE_BYTES {
135 let _ = event_tx
136 .send(SidecarLifecycleEvent::Error(format!(
137 "sidecar stdout line exceeded {MAX_SIDECAR_LINE_BYTES} bytes, dropped"
138 )))
139 .await;
140 continue;
141 }
142 let line = line.trim_end();
143 if line.trim().is_empty() {
144 continue;
145 }
146 let event = match serde_json::from_str::<SidecarFrame>(line) {
147 Ok(ev) => ev,
148 Err(err) => {
149 let _ = event_tx
150 .send(SidecarLifecycleEvent::Error(format!(
151 "failed to parse sidecar line: {err}: {line}"
152 )))
153 .await;
154 continue;
155 }
156 };
157 let mapped = match event {
158 SidecarFrame::Hello {
159 protocol_version,
160 extension,
161 capabilities,
162 } => {
163 if protocol_version < SIDECAR_PROTOCOL_VERSION {
164 Some(SidecarLifecycleEvent::Error(format!(
165 "sidecar protocol v{protocol_version} is too old; host requires v{SIDECAR_PROTOCOL_VERSION}. Update the plugin via /plugins."
166 )))
167 } else {
168 Some(SidecarLifecycleEvent::Ready {
169 protocol_version,
170 extension,
171 capabilities,
172 })
173 }
174 }
175 SidecarFrame::Status { state, label, .. } => {
176 Some(SidecarLifecycleEvent::StateChanged { state, label })
177 }
178 SidecarFrame::InsertText { text, mode } => {
179 Some(SidecarLifecycleEvent::InsertText { text, mode })
180 }
181 SidecarFrame::Error { message } => Some(SidecarLifecycleEvent::Error(message)),
182 SidecarFrame::Custom => None,
183 };
184 if let Some(event) = mapped {
185 if event_tx.send(event).await.is_err() {
186 break;
188 }
189 }
190 }
191 let _ = event_tx.send(SidecarLifecycleEvent::Exited).await;
192 });
193
194 let stderr_handle = stderr.map(|stderr| {
196 tokio::spawn(async move {
197 let mut reader = BufReader::new(stderr);
198 let mut line = String::new();
199 loop {
200 line.clear();
201 let mut limited = (&mut reader).take(MAX_SIDECAR_LINE_BYTES + 1);
202 match tokio::io::AsyncBufReadExt::read_line(&mut limited, &mut line).await {
203 Ok(0) => break,
204 Ok(_) => {}
205 Err(_) => break,
206 }
207 if line.len() as u64 > MAX_SIDECAR_LINE_BYTES {
208 tracing::warn!(target: "sidecar::manager", "stderr line exceeded bound, dropped");
209 continue;
210 }
211 tracing::debug!(target: "sidecar::manager", "{}", line.trim_end());
212 }
213 })
214 });
215
216 let mut manager = Self {
217 hello_capabilities: Vec::new(),
218 child: Some(child),
219 stdin: Some(stdin),
220 rx,
221 reader_handle: Some(reader_handle),
222 stderr_handle,
223 };
224
225 let hello_timeout =
230 tokio::time::timeout(std::time::Duration::from_secs(10), manager.rx.recv())
231 .await
232 .map_err(|_| {
233 SidecarError::Protocol("sidecar did not send Hello within 10s".to_string())
234 })?;
235
236 match hello_timeout {
237 Some(SidecarLifecycleEvent::Ready { capabilities, .. }) => {
238 manager.hello_capabilities = capabilities;
239 }
240 Some(SidecarLifecycleEvent::Error(e)) => {
241 return Err(SidecarError::Protocol(format!("sidecar Hello failed: {e}")));
242 }
243 Some(SidecarLifecycleEvent::Exited) | None => {
244 return Err(SidecarError::Protocol(
245 "sidecar exited before sending Hello".to_string(),
246 ));
247 }
248 Some(other) => {
249 return Err(SidecarError::Protocol(format!(
250 "expected Hello from sidecar, got: {:?}",
251 other
252 )));
253 }
254 }
255
256 manager.send(SidecarCommand::Init { config }).await?;
257 Ok(manager)
258 }
259
260 pub fn ready_after_init(&self) -> bool {
263 self.hello_capabilities
264 .iter()
265 .any(|c| c == "ready_after_init")
266 }
267
268 pub fn try_next_event(&mut self) -> Option<SidecarLifecycleEvent> {
270 match self.rx.try_recv() {
271 Ok(event) => Some(event),
272 Err(mpsc::error::TryRecvError::Disconnected) => Some(SidecarLifecycleEvent::Exited),
273 Err(mpsc::error::TryRecvError::Empty) => None,
274 }
275 }
276
277 pub async fn press(&mut self) -> Result<(), SidecarError> {
279 self.send(SidecarCommand::Trigger {
280 name: "press".into(),
281 payload: None,
282 })
283 .await
284 }
285
286 pub async fn release(&mut self) -> Result<(), SidecarError> {
288 self.send(SidecarCommand::Trigger {
289 name: "release".into(),
290 payload: None,
291 })
292 .await
293 }
294
295 pub async fn shutdown(&mut self) -> Result<(), SidecarError> {
299 let send_result = match self.send(SidecarCommand::Shutdown).await {
300 Err(SidecarError::AlreadyShutDown) => Ok(()),
301 result => result,
302 };
303 drop(self.stdin.take());
306 if let Some(mut child) = self.child.take() {
307 match tokio::time::timeout(std::time::Duration::from_secs(2), child.wait()).await {
309 Ok(_) => {} Err(_) => {
311 let _ = child.kill().await;
312 }
313 }
314 }
315 if let Some(handle) = self.reader_handle.take() {
316 handle.abort();
317 }
318 if let Some(handle) = self.stderr_handle.take() {
319 handle.abort();
320 }
321 send_result
322 }
323
324 pub async fn next_event(&mut self) -> Option<SidecarLifecycleEvent> {
327 self.rx.recv().await
328 }
329
330 async fn send(&mut self, cmd: SidecarCommand) -> Result<(), SidecarError> {
331 let mut buf = serde_json::to_vec(&cmd)?;
332 buf.push(b'\n');
333 let mut stdin = self.stdin.take().ok_or(SidecarError::AlreadyShutDown)?;
337 tokio::time::timeout(COMMAND_WRITE_TIMEOUT, async {
338 stdin.write_all(&buf).await?;
339 stdin.flush().await
340 })
341 .await
342 .map_err(|_| {
343 std::io::Error::new(
344 std::io::ErrorKind::TimedOut,
345 "sidecar command write did not complete within 2s; command input closed",
346 )
347 })??;
348 self.stdin = Some(stdin);
349 Ok(())
350 }
351}
352
353impl Drop for SidecarManager {
354 fn drop(&mut self) {
355 if let Some(handle) = self.reader_handle.take() {
357 handle.abort();
358 }
359 if let Some(handle) = self.stderr_handle.take() {
360 handle.abort();
361 }
362 }
363}