1use std::collections::{HashMap, VecDeque};
11use std::process::{ExitStatus, Stdio};
12use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
13use std::sync::{Arc, Mutex};
14use std::time::Duration;
15
16use serde::Serialize;
17use serde_json::{Map, Value};
18use thiserror::Error;
19use tokio::io::{
20 AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader,
21};
22use tokio::process::Command;
23use tokio::sync::{mpsc, oneshot, watch, Mutex as AsyncMutex};
24
25const INITIALIZING: u8 = 1;
26const INITIALIZED: u8 = 2;
27const INITIALIZE_FAILED: u8 = 3;
28
29#[derive(Debug, Clone, Copy)]
31pub struct AppServerLimits {
32 pub max_frame_bytes: usize,
33 pub max_pending_requests: usize,
34 pub outbound_queue_capacity: usize,
35 pub inbound_queue_capacity: usize,
36 pub max_stderr_bytes: usize,
37}
38
39impl Default for AppServerLimits {
40 fn default() -> Self {
41 Self {
42 max_frame_bytes: 4 * 1024 * 1024,
43 max_pending_requests: 128,
44 outbound_queue_capacity: 128,
45 inbound_queue_capacity: 256,
46 max_stderr_bytes: 64 * 1024,
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum RpcId {
53 Number(String),
54 String(String),
55 Null,
56}
57
58impl RpcId {
59 pub(crate) fn from_value(value: &Value) -> Result<Self, AppServerError> {
60 match value {
61 Value::Number(number) if number.is_i64() || number.is_u64() => {
62 Ok(Self::Number(number.to_string()))
63 }
64 Value::String(value) => Ok(Self::String(value.clone())),
65 Value::Null => Ok(Self::Null),
66 _ => Err(AppServerError::Protocol(
67 "JSON-RPC id must be an integer, string, or null".to_string(),
68 )),
69 }
70 }
71
72 fn outgoing_u64(&self) -> Option<u64> {
73 match self {
74 Self::Number(value) => value.parse().ok(),
75 Self::String(_) | Self::Null => None,
76 }
77 }
78
79 fn to_value(&self) -> Value {
80 match self {
81 Self::Number(value) => serde_json::from_str(value).unwrap_or(Value::Null),
82 Self::String(value) => Value::String(value.clone()),
83 Self::Null => Value::Null,
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq)]
89pub enum AppServerIncoming {
90 Notification {
91 method: String,
92 params: Value,
93 },
94 Request {
95 id: RpcId,
96 method: String,
97 params: Value,
98 },
99}
100
101#[derive(Debug, Clone, PartialEq, Error)]
102pub enum AppServerError {
103 #[error("failed to spawn App Server: {0}")]
104 Spawn(String),
105 #[error("App Server I/O failed: {0}")]
106 Io(String),
107 #[error("App Server protocol violation: {0}")]
108 Protocol(String),
109 #[error("App Server request limit reached ({0})")]
110 PendingRequestLimit(usize),
111 #[error("App Server request timed out after {0:?}")]
112 Timeout(Duration),
113 #[error("App Server request was rejected ({code}): {message}")]
114 Remote {
115 code: i64,
116 message: String,
117 data: Option<Value>,
118 },
119 #[error("App Server transport is closed")]
120 Closed,
121 #[error("App Server initialize may only be attempted once")]
122 InitializeAlreadyAttempted,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum AppServerExit {
127 Clean {
128 code: Option<i32>,
129 stderr_tail: String,
130 },
131 ForcedAfterTimeout {
132 stderr_tail: String,
133 },
134 BeforeInitialize {
135 code: Option<i32>,
136 stderr_tail: String,
137 },
138 NonZero {
139 code: Option<i32>,
140 stderr_tail: String,
141 },
142 ProtocolViolation {
143 message: String,
144 stderr_tail: String,
145 },
146 UnexpectedEof {
147 stderr_tail: String,
148 },
149}
150
151type PendingSender = oneshot::Sender<Result<Value, AppServerError>>;
152
153struct SharedState {
154 pending: Mutex<HashMap<u64, PendingSender>>,
155 failure: Mutex<Option<AppServerError>>,
156 closing: AtomicBool,
157 closed: AtomicBool,
158}
159
160struct PendingGuard {
161 shared: Arc<SharedState>,
162 id: u64,
163}
164
165impl Drop for PendingGuard {
166 fn drop(&mut self) {
167 self.shared.pending.lock().unwrap().remove(&self.id);
168 }
169}
170
171enum Outbound {
172 Frame {
173 bytes: Vec<u8>,
174 written: oneshot::Sender<Result<(), AppServerError>>,
175 },
176 Close {
177 closed: oneshot::Sender<Result<(), AppServerError>>,
178 },
179}
180
181pub struct AppServerTransport {
182 outbound_tx: mpsc::Sender<Outbound>,
183 incoming_rx: AsyncMutex<mpsc::Receiver<AppServerIncoming>>,
184 reader_task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
185 shared: Arc<SharedState>,
186 next_request_id: AtomicU64,
187 initialize_state: AtomicU8,
188 max_frame_bytes: usize,
189 max_pending_requests: usize,
190}
191
192impl AppServerTransport {
193 pub fn new<R, W>(reader: R, writer: W, limits: AppServerLimits) -> Self
194 where
195 R: AsyncRead + Unpin + Send + 'static,
196 W: AsyncWrite + Unpin + Send + 'static,
197 {
198 let shared = Arc::new(SharedState {
199 pending: Mutex::new(HashMap::new()),
200 failure: Mutex::new(None),
201 closing: AtomicBool::new(false),
202 closed: AtomicBool::new(false),
203 });
204 let (outbound_tx, outbound_rx) = mpsc::channel(limits.outbound_queue_capacity.max(1));
205 let (incoming_tx, incoming_rx) = mpsc::channel(limits.inbound_queue_capacity.max(1));
206
207 tokio::spawn(run_writer(writer, outbound_rx, shared.clone()));
208 let reader_task = tokio::spawn(run_reader(
209 reader,
210 incoming_tx,
211 shared.clone(),
212 limits.max_frame_bytes,
213 ));
214
215 Self {
216 outbound_tx,
217 incoming_rx: AsyncMutex::new(incoming_rx),
218 reader_task: AsyncMutex::new(Some(reader_task)),
219 shared,
220 next_request_id: AtomicU64::new(1),
221 initialize_state: AtomicU8::new(0),
222 max_frame_bytes: limits.max_frame_bytes,
223 max_pending_requests: limits.max_pending_requests,
224 }
225 }
226
227 pub fn is_initialized(&self) -> bool {
228 self.initialize_state.load(Ordering::Acquire) == INITIALIZED
229 }
230
231 pub fn failure(&self) -> Option<AppServerError> {
232 self.shared.failure.lock().unwrap().clone()
233 }
234
235 pub async fn next_incoming(&self) -> Option<AppServerIncoming> {
236 self.incoming_rx.lock().await.recv().await
237 }
238
239 async fn wait_for_reader_to_finish(&self) {
249 if let Some(task) = self.reader_task.lock().await.take() {
250 let _ = task.await;
251 }
252 }
253
254 pub async fn request(
255 &self,
256 method: &str,
257 params: Value,
258 request_timeout: Duration,
259 ) -> Result<Value, AppServerError> {
260 if let Some(error) = self.failure() {
261 return Err(error);
262 }
263 if self.shared.closed.load(Ordering::Acquire) {
264 return Err(AppServerError::Closed);
265 }
266
267 let id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
268 let (response_tx, response_rx) = oneshot::channel();
269 {
270 let mut pending = self.shared.pending.lock().unwrap();
271 if pending.len() >= self.max_pending_requests {
272 return Err(AppServerError::PendingRequestLimit(
273 self.max_pending_requests,
274 ));
275 }
276 pending.insert(id, response_tx);
277 }
278 let _pending_guard = PendingGuard {
279 shared: self.shared.clone(),
280 id,
281 };
282
283 let frame = serde_json::json!({
284 "id": id,
285 "method": method,
286 "params": params,
287 });
288
289 let operation = async {
290 self.write_value(frame).await?;
291 response_rx.await.unwrap_or(Err(AppServerError::Closed))
292 };
293
294 match tokio::time::timeout(request_timeout, operation).await {
295 Ok(result) => result,
296 Err(_) => Err(AppServerError::Timeout(request_timeout)),
297 }
298 }
299
300 pub async fn notify(
301 &self,
302 method: &str,
303 params: Value,
304 write_timeout: Duration,
305 ) -> Result<(), AppServerError> {
306 let mut frame = Map::from_iter([("method".to_string(), Value::String(method.to_string()))]);
307 if !params.is_null() {
308 frame.insert("params".to_string(), params);
309 }
310 tokio::time::timeout(write_timeout, self.write_value(Value::Object(frame)))
311 .await
312 .map_err(|_| AppServerError::Timeout(write_timeout))?
313 }
314
315 #[cfg(test)]
318 pub async fn respond(
319 &self,
320 id: RpcId,
321 result: Result<Value, ServerResponseError>,
322 write_timeout: Duration,
323 ) -> Result<(), AppServerError> {
324 self.respond_tracked(id, result, write_timeout, &WriteOutcomeTracker::new())
325 .await
326 }
327
328 pub async fn respond_tracked(
337 &self,
338 id: RpcId,
339 result: Result<Value, ServerResponseError>,
340 write_timeout: Duration,
341 outcome: &WriteOutcomeTracker,
342 ) -> Result<(), AppServerError> {
343 let mut frame = Map::new();
344 frame.insert("id".to_string(), id.to_value());
345 match result {
346 Ok(result) => {
347 frame.insert("result".to_string(), result);
348 }
349 Err(error) => {
350 frame.insert(
351 "error".to_string(),
352 serde_json::to_value(error)
353 .map_err(|error| AppServerError::Protocol(error.to_string()))?,
354 );
355 }
356 }
357 tokio::time::timeout(
358 write_timeout,
359 self.write_value_tracked(Value::Object(frame), outcome),
360 )
361 .await
362 .map_err(|_| AppServerError::Timeout(write_timeout))?
363 }
364
365 pub async fn initialize(&self, timeout: Duration) -> Result<Value, AppServerError> {
366 if self
367 .initialize_state
368 .compare_exchange(0, INITIALIZING, Ordering::AcqRel, Ordering::Acquire)
369 .is_err()
370 {
371 return Err(AppServerError::InitializeAlreadyAttempted);
372 }
373
374 let params = InitializeParams {
375 client_info: ClientInfo {
376 name: "agentmux",
377 title: Some("AgentMux"),
378 version: env!("CARGO_PKG_VERSION"),
379 },
380 capabilities: InitializeCapabilities {
381 experimental_api: false,
382 },
383 };
384 let params = serde_json::to_value(params)
385 .map_err(|error| AppServerError::Protocol(error.to_string()))?;
386
387 let result = match self.request("initialize", params, timeout).await {
388 Ok(result) => result,
389 Err(error) => {
390 self.initialize_state
391 .store(INITIALIZE_FAILED, Ordering::Release);
392 return Err(error);
393 }
394 };
395 if let Err(error) = self.notify("initialized", Value::Null, timeout).await {
396 self.initialize_state
397 .store(INITIALIZE_FAILED, Ordering::Release);
398 return Err(error);
399 }
400 self.initialize_state.store(INITIALIZED, Ordering::Release);
401 Ok(result)
402 }
403
404 pub async fn shutdown(&self, timeout: Duration) -> Result<(), AppServerError> {
405 self.shared.closing.store(true, Ordering::Release);
406 let operation = async {
407 let (closed_tx, closed_rx) = oneshot::channel();
408 self.outbound_tx
409 .send(Outbound::Close { closed: closed_tx })
410 .await
411 .map_err(|_| AppServerError::Closed)?;
412 closed_rx.await.unwrap_or(Err(AppServerError::Closed))
413 };
414 let result = tokio::time::timeout(timeout, operation)
415 .await
416 .map_err(|_| AppServerError::Timeout(timeout))?;
417 fail_transport(&self.shared, AppServerError::Closed);
418 result
419 }
420
421 async fn write_value(&self, value: Value) -> Result<(), AppServerError> {
422 self.write_value_tracked(value, &WriteOutcomeTracker::new())
423 .await
424 }
425
426 async fn write_value_tracked(
432 &self,
433 value: Value,
434 outcome: &WriteOutcomeTracker,
435 ) -> Result<(), AppServerError> {
436 if let Some(error) = self.failure() {
437 return Err(error);
438 }
439 if self.shared.closing.load(Ordering::Acquire) || self.shared.closed.load(Ordering::Acquire)
440 {
441 return Err(AppServerError::Closed);
442 }
443 let mut bytes = serde_json::to_vec(&value)
444 .map_err(|error| AppServerError::Protocol(error.to_string()))?;
445 if bytes.len() > self.max_frame_bytes {
446 return Err(AppServerError::Protocol(format!(
447 "outbound frame is {} bytes; limit is {}",
448 bytes.len(),
449 self.max_frame_bytes
450 )));
451 }
452 bytes.push(b'\n');
453 let (written_tx, written_rx) = oneshot::channel();
454 self.outbound_tx
455 .send(Outbound::Frame {
456 bytes,
457 written: written_tx,
458 })
459 .await
460 .map_err(|_| AppServerError::Closed)?;
461 outcome.mark_queued();
467 let result = written_rx.await.unwrap_or(Err(AppServerError::Closed));
468 if result.is_err() {
469 outcome.mark_confirmed_undelivered();
472 }
473 result
474 }
475}
476
477#[derive(Debug)]
486pub struct WriteOutcomeTracker(AtomicU8);
487
488impl WriteOutcomeTracker {
489 const NOT_QUEUED: u8 = 0;
490 const OUTCOME_UNCONFIRMED: u8 = 1;
491 const CONFIRMED_UNDELIVERED: u8 = 2;
492
493 pub fn new() -> Self {
494 Self(AtomicU8::new(Self::NOT_QUEUED))
495 }
496
497 fn mark_queued(&self) {
498 self.0.store(Self::OUTCOME_UNCONFIRMED, Ordering::Release);
499 }
500
501 fn mark_confirmed_undelivered(&self) {
502 self.0
503 .store(Self::CONFIRMED_UNDELIVERED, Ordering::Release);
504 }
505
506 pub fn safe_to_retry(&self) -> bool {
512 matches!(
513 self.0.load(Ordering::Acquire),
514 Self::NOT_QUEUED | Self::CONFIRMED_UNDELIVERED
515 )
516 }
517}
518
519#[derive(Debug, Clone, Serialize)]
520pub struct ServerResponseError {
521 pub code: i64,
522 pub message: String,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub data: Option<Value>,
525}
526
527#[derive(Serialize)]
528#[serde(rename_all = "camelCase")]
529struct InitializeParams<'a> {
530 client_info: ClientInfo<'a>,
531 capabilities: InitializeCapabilities,
532}
533
534#[derive(Serialize)]
535struct ClientInfo<'a> {
536 name: &'a str,
537 title: Option<&'a str>,
538 version: &'a str,
539}
540
541#[derive(Serialize)]
542#[serde(rename_all = "camelCase")]
543struct InitializeCapabilities {
544 experimental_api: bool,
545}
546
547async fn run_writer<W>(
548 mut writer: W,
549 mut outbound_rx: mpsc::Receiver<Outbound>,
550 shared: Arc<SharedState>,
551) where
552 W: AsyncWrite + Unpin,
553{
554 while let Some(outbound) = outbound_rx.recv().await {
555 match outbound {
556 Outbound::Frame { bytes, written } => {
557 let result = async {
558 writer.write_all(&bytes).await?;
559 writer.flush().await
560 }
561 .await
562 .map_err(|error: std::io::Error| AppServerError::Io(error.to_string()));
563 let _ = written.send(result.clone());
564 if let Err(error) = result {
565 fail_transport(&shared, error);
566 break;
567 }
568 }
569 Outbound::Close { closed } => {
570 let result = writer
571 .shutdown()
572 .await
573 .map_err(|error| AppServerError::Io(error.to_string()));
574 let _ = closed.send(result.clone());
575 if let Err(error) = result {
576 fail_transport(&shared, error);
577 }
578 break;
579 }
580 }
581 }
582 shared.closed.store(true, Ordering::Release);
583}
584
585async fn run_reader<R>(
586 reader: R,
587 incoming_tx: mpsc::Sender<AppServerIncoming>,
588 shared: Arc<SharedState>,
589 max_frame_bytes: usize,
590) where
591 R: AsyncRead + Unpin,
592{
593 let mut reader = BufReader::new(reader);
594 loop {
595 let frame = match read_frame(&mut reader, max_frame_bytes).await {
596 Ok(Some(frame)) => frame,
597 Ok(None) if shared.closing.load(Ordering::Acquire) => break,
598 Ok(None) => {
599 fail_transport(&shared, AppServerError::Closed);
600 break;
601 }
602 Err(error) => {
603 fail_transport(&shared, error);
604 break;
605 }
606 };
607
608 match classify_frame(&frame) {
609 Ok(WireMessage::Response { id, result }) => {
610 let Some(id) = id.outgoing_u64() else {
611 tracing::warn!(?id, "ignoring App Server response with non-client id");
612 continue;
613 };
614 let pending = shared.pending.lock().unwrap().remove(&id);
615 if let Some(pending) = pending {
616 let _ = pending.send(result);
617 } else {
618 tracing::warn!(
619 id,
620 "ignoring duplicate, unknown, or cancelled App Server response"
621 );
622 }
623 }
624 Ok(WireMessage::Incoming(incoming)) => {
625 if incoming_tx.try_send(incoming).is_err() {
626 fail_transport(
627 &shared,
628 AppServerError::Protocol(
629 "inbound App Server event queue exceeded its bound".to_string(),
630 ),
631 );
632 break;
633 }
634 }
635 Err(error) => {
636 fail_transport(&shared, error);
637 break;
638 }
639 }
640 }
641}
642
643async fn read_frame<R>(
644 reader: &mut R,
645 max_frame_bytes: usize,
646) -> Result<Option<Vec<u8>>, AppServerError>
647where
648 R: AsyncBufRead + Unpin,
649{
650 let mut frame = Vec::new();
651 loop {
652 let available = reader
653 .fill_buf()
654 .await
655 .map_err(|error| AppServerError::Io(error.to_string()))?;
656 if available.is_empty() {
657 return if frame.is_empty() {
658 Ok(None)
659 } else {
660 Ok(Some(frame))
661 };
662 }
663
664 let newline = available.iter().position(|byte| *byte == b'\n');
665 let consumed = newline.map_or(available.len(), |index| index + 1);
666 let payload_bytes = newline.map_or(consumed, |index| index);
667 if frame.len().saturating_add(payload_bytes) > max_frame_bytes {
668 return Err(AppServerError::Protocol(format!(
669 "inbound frame exceeds {max_frame_bytes} bytes"
670 )));
671 }
672 frame.extend_from_slice(&available[..payload_bytes]);
673 reader.consume(consumed);
674
675 if newline.is_some() {
676 if frame.last() == Some(&b'\r') {
677 frame.pop();
678 }
679 return Ok(Some(frame));
680 }
681 }
682}
683
684enum WireMessage {
685 Response {
686 id: RpcId,
687 result: Result<Value, AppServerError>,
688 },
689 Incoming(AppServerIncoming),
690}
691
692fn classify_frame(frame: &[u8]) -> Result<WireMessage, AppServerError> {
693 if frame.is_empty() {
694 return Err(AppServerError::Protocol(
695 "stdout contained an empty frame".to_string(),
696 ));
697 }
698 let value: Value = serde_json::from_slice(frame)
699 .map_err(|error| AppServerError::Protocol(format!("invalid JSON frame: {error}")))?;
700 let object = value
701 .as_object()
702 .ok_or_else(|| AppServerError::Protocol("JSON-RPC frame must be an object".to_string()))?;
703 validate_jsonrpc_marker(object)?;
704
705 if let Some(method) = object.get("method") {
706 let method = method.as_str().ok_or_else(|| {
707 AppServerError::Protocol("JSON-RPC method must be a string".to_string())
708 })?;
709 let params = object.get("params").cloned().unwrap_or(Value::Null);
710 return if let Some(id) = object.get("id") {
711 Ok(WireMessage::Incoming(AppServerIncoming::Request {
712 id: RpcId::from_value(id)?,
713 method: method.to_string(),
714 params,
715 }))
716 } else {
717 Ok(WireMessage::Incoming(AppServerIncoming::Notification {
718 method: method.to_string(),
719 params,
720 }))
721 };
722 }
723
724 let id = object
725 .get("id")
726 .ok_or_else(|| AppServerError::Protocol("response is missing an id".to_string()))?;
727 let id = RpcId::from_value(id)?;
728 let has_result = object.contains_key("result");
729 let has_error = object.contains_key("error");
730 if has_result == has_error {
731 return Err(AppServerError::Protocol(
732 "response must contain exactly one of result or error".to_string(),
733 ));
734 }
735 let result = if has_result {
736 Ok(object.get("result").cloned().unwrap_or(Value::Null))
737 } else {
738 Err(parse_remote_error(object.get("error").unwrap())?)
739 };
740 Ok(WireMessage::Response { id, result })
741}
742
743fn validate_jsonrpc_marker(object: &Map<String, Value>) -> Result<(), AppServerError> {
744 match object.get("jsonrpc") {
745 None => Ok(()), Some(Value::String(version)) if version == "2.0" => Ok(()),
747 Some(_) => Err(AppServerError::Protocol(
748 "jsonrpc, when present, must equal \"2.0\"".to_string(),
749 )),
750 }
751}
752
753fn parse_remote_error(value: &Value) -> Result<AppServerError, AppServerError> {
754 let error = value
755 .as_object()
756 .ok_or_else(|| AppServerError::Protocol("response error must be an object".to_string()))?;
757 let code = error.get("code").and_then(Value::as_i64).ok_or_else(|| {
758 AppServerError::Protocol("response error code must be an integer".to_string())
759 })?;
760 let message = error
761 .get("message")
762 .and_then(Value::as_str)
763 .ok_or_else(|| {
764 AppServerError::Protocol("response error message must be a string".to_string())
765 })?;
766 Ok(AppServerError::Remote {
767 code,
768 message: message.to_string(),
769 data: error.get("data").cloned(),
770 })
771}
772
773fn fail_transport(shared: &SharedState, error: AppServerError) {
774 let failure = {
775 let mut failure = shared.failure.lock().unwrap();
776 if failure.is_none() {
777 *failure = Some(error.clone());
778 }
779 failure.clone().unwrap()
780 };
781 shared.closed.store(true, Ordering::Release);
782 let pending = std::mem::take(&mut *shared.pending.lock().unwrap());
783 for (_, sender) in pending {
784 let _ = sender.send(Err(failure.clone()));
785 }
786}
787
788struct StderrTail {
789 bytes: VecDeque<u8>,
790 max_bytes: usize,
791}
792
793impl StderrTail {
794 fn push(&mut self, chunk: &[u8]) {
795 self.bytes.extend(chunk);
796 while self.bytes.len() > self.max_bytes {
797 self.bytes.pop_front();
798 }
799 }
800
801 fn snapshot(&self) -> String {
802 let bytes: Vec<u8> = self.bytes.iter().copied().collect();
803 String::from_utf8_lossy(&bytes).into_owned()
804 }
805}
806
807pub struct AppServerProcess {
808 pub transport: Arc<AppServerTransport>,
814 exit_status: watch::Receiver<Option<Result<ExitStatus, String>>>,
828 stderr_tail: Arc<Mutex<StderrTail>>,
829 stderr_task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
830 shutdown_requested: AtomicBool,
831 pid: Option<u32>,
832}
833
834impl AppServerProcess {
835 pub fn spawn(mut command: Command, limits: AppServerLimits) -> Result<Self, AppServerError> {
836 command.kill_on_drop(true);
837 command.stdin(Stdio::piped());
838 command.stdout(Stdio::piped());
839 command.stderr(Stdio::piped());
840 let mut child = command
841 .spawn()
842 .map_err(|error| AppServerError::Spawn(error.to_string()))?;
843 let pid = child.id();
844 let stdin = child
845 .stdin
846 .take()
847 .ok_or_else(|| AppServerError::Spawn("stdin was not captured".to_string()))?;
848 let stdout = child
849 .stdout
850 .take()
851 .ok_or_else(|| AppServerError::Spawn("stdout was not captured".to_string()))?;
852 let mut stderr = child
853 .stderr
854 .take()
855 .ok_or_else(|| AppServerError::Spawn("stderr was not captured".to_string()))?;
856
857 let stderr_tail = Arc::new(Mutex::new(StderrTail {
858 bytes: VecDeque::new(),
859 max_bytes: limits.max_stderr_bytes,
860 }));
861 let stderr_tail_task = stderr_tail.clone();
862 let stderr_task = tokio::spawn(async move {
863 let mut buffer = [0_u8; 4096];
864 loop {
865 match stderr.read(&mut buffer).await {
866 Ok(0) => break,
867 Ok(read) => stderr_tail_task.lock().unwrap().push(&buffer[..read]),
868 Err(error) => {
869 tracing::warn!(%error, "App Server stderr read failed");
870 break;
871 }
872 }
873 }
874 });
875
876 let transport = Arc::new(AppServerTransport::new(stdout, stdin, limits));
878
879 let (exit_tx, exit_rx) = watch::channel(None);
880 tokio::spawn(async move {
881 let result = child
882 .wait()
883 .await
884 .map_err(|error| error.to_string());
885 let _ = exit_tx.send(Some(result));
889 });
890
891 Ok(Self {
892 transport,
893 exit_status: exit_rx,
894 stderr_tail,
895 stderr_task: AsyncMutex::new(Some(stderr_task)),
896 shutdown_requested: AtomicBool::new(false),
897 pid,
898 })
899 }
900
901 pub fn pid(&self) -> Option<u32> {
902 self.pid
903 }
904
905 async fn await_exit(&self) -> Result<ExitStatus, AppServerError> {
909 let mut rx = self.exit_status.clone();
910 loop {
911 if let Some(result) = rx.borrow().clone() {
912 return result.map_err(AppServerError::Io);
913 }
914 if rx.changed().await.is_err() {
915 return Err(AppServerError::Closed);
916 }
917 }
918 }
919
920 pub async fn wait_for_exit(&self) -> Result<AppServerExit, AppServerError> {
921 let status = self.await_exit().await?;
922 self.transport.wait_for_reader_to_finish().await;
923 self.finish_stderr().await;
924 Ok(self.classify_exit(status))
925 }
926
927 pub async fn shutdown(&self, timeout: Duration) -> Result<AppServerExit, AppServerError> {
928 self.shutdown_requested.store(true, Ordering::Release);
929 let _ = self.transport.shutdown(timeout).await;
930 let status = match tokio::time::timeout(timeout, self.await_exit()).await {
931 Ok(result) => Some(result?),
932 Err(_) => {
933 if let Some(pid) = self.pid {
940 let _ = agentmux_common::process::kill_pid(pid);
941 }
942 let _ = self.await_exit().await;
947 None
948 }
949 };
950 self.transport.wait_for_reader_to_finish().await;
951 self.finish_stderr().await;
952 match status {
953 Some(status) => Ok(self.classify_exit(status)),
954 None => Ok(AppServerExit::ForcedAfterTimeout {
955 stderr_tail: self.stderr_snapshot(),
956 }),
957 }
958 }
959
960 async fn finish_stderr(&self) {
961 if let Some(task) = self.stderr_task.lock().await.take() {
962 let _ = task.await;
963 }
964 }
965
966 fn stderr_snapshot(&self) -> String {
967 self.stderr_tail.lock().unwrap().snapshot()
968 }
969
970 fn classify_exit(&self, status: ExitStatus) -> AppServerExit {
971 let stderr_tail = self.stderr_snapshot();
972 if let Some(AppServerError::Protocol(message)) = self.transport.failure() {
973 return AppServerExit::ProtocolViolation {
974 message,
975 stderr_tail,
976 };
977 }
978 if !self.transport.is_initialized() {
979 return AppServerExit::BeforeInitialize {
980 code: status.code(),
981 stderr_tail,
982 };
983 }
984 if !status.success() {
985 return AppServerExit::NonZero {
986 code: status.code(),
987 stderr_tail,
988 };
989 }
990 if self.shutdown_requested.load(Ordering::Acquire) {
991 AppServerExit::Clean {
992 code: status.code(),
993 stderr_tail,
994 }
995 } else {
996 AppServerExit::UnexpectedEof { stderr_tail }
997 }
998 }
999}
1000
1001#[cfg(test)]
1002mod tests {
1003 use serde_json::json;
1004 use tokio::io::{duplex, AsyncBufReadExt, AsyncWriteExt, BufReader};
1005
1006 use super::*;
1007
1008 #[tokio::test]
1009 async fn correlates_out_of_order_responses_and_preserves_notifications() {
1010 let (client_io, server_io) = duplex(8192);
1011 let (client_reader, client_writer) = tokio::io::split(client_io);
1012 let (server_reader, mut server_writer) = tokio::io::split(server_io);
1013 let transport = Arc::new(AppServerTransport::new(
1014 client_reader,
1015 client_writer,
1016 AppServerLimits::default(),
1017 ));
1018
1019 let server = tokio::spawn(async move {
1020 let mut lines = BufReader::new(server_reader).lines();
1021 let first: Value =
1022 serde_json::from_str(&lines.next_line().await.unwrap().unwrap()).unwrap();
1023 let second: Value =
1024 serde_json::from_str(&lines.next_line().await.unwrap().unwrap()).unwrap();
1025 let (first_id, second_id) = if first["method"] == "one" {
1026 (&first["id"], &second["id"])
1027 } else {
1028 (&second["id"], &first["id"])
1029 };
1030 let frames = format!(
1031 "{{\"method\":\"thread/status/changed\",\"params\":{{\"ok\":true}}}}\r\n{{\"id\":{},\"result\":\"second\"}}\n{{\"id\":{},\"result\":\"first\"}}\n",
1032 second_id, first_id
1033 );
1034 server_writer
1035 .write_all(&frames.as_bytes()[..17])
1036 .await
1037 .unwrap();
1038 server_writer
1039 .write_all(&frames.as_bytes()[17..])
1040 .await
1041 .unwrap();
1042 });
1043
1044 let first = {
1045 let transport = transport.clone();
1046 tokio::spawn(async move {
1047 transport
1048 .request("one", Value::Null, Duration::from_secs(1))
1049 .await
1050 })
1051 };
1052 let second = {
1053 let transport = transport.clone();
1054 tokio::spawn(async move {
1055 transport
1056 .request("two", Value::Null, Duration::from_secs(1))
1057 .await
1058 })
1059 };
1060
1061 assert_eq!(first.await.unwrap().unwrap(), Value::String("first".into()));
1062 assert_eq!(
1063 second.await.unwrap().unwrap(),
1064 Value::String("second".into())
1065 );
1066 assert_eq!(
1067 transport.next_incoming().await,
1068 Some(AppServerIncoming::Notification {
1069 method: "thread/status/changed".to_string(),
1070 params: serde_json::json!({ "ok": true }),
1071 })
1072 );
1073 server.await.unwrap();
1074 }
1075
1076 #[tokio::test]
1077 async fn remote_errors_resolve_only_the_matching_request() {
1078 let (client_io, server_io) = duplex(4096);
1079 let (client_reader, client_writer) = tokio::io::split(client_io);
1080 let (server_reader, mut server_writer) = tokio::io::split(server_io);
1081 let transport =
1082 AppServerTransport::new(client_reader, client_writer, AppServerLimits::default());
1083 let server = tokio::spawn(async move {
1084 let request = BufReader::new(server_reader)
1085 .lines()
1086 .next_line()
1087 .await
1088 .unwrap()
1089 .unwrap();
1090 let request: Value = serde_json::from_str(&request).unwrap();
1091 server_writer
1092 .write_all(
1093 format!(
1094 "{{\"id\":{},\"error\":{{\"code\":-32001,\"message\":\"no thread\",\"data\":{{\"retry\":false}}}}}}\n",
1095 request["id"]
1096 )
1097 .as_bytes(),
1098 )
1099 .await
1100 .unwrap();
1101 });
1102
1103 assert_eq!(
1104 transport
1105 .request("thread/resume", Value::Null, Duration::from_secs(1))
1106 .await,
1107 Err(AppServerError::Remote {
1108 code: -32001,
1109 message: "no thread".to_string(),
1110 data: Some(serde_json::json!({ "retry": false })),
1111 })
1112 );
1113 server.await.unwrap();
1114 }
1115
1116 #[tokio::test]
1117 async fn responds_to_server_requests_without_auto_approving_them() {
1118 let (client_io, server_io) = duplex(4096);
1119 let (client_reader, client_writer) = tokio::io::split(client_io);
1120 let (server_reader, mut server_writer) = tokio::io::split(server_io);
1121 let transport =
1122 AppServerTransport::new(client_reader, client_writer, AppServerLimits::default());
1123 server_writer
1124 .write_all(
1125 br#"{"id":"approval-1","method":"item/commandExecution/requestApproval","params":{"command":"echo hi"}}
1126"#,
1127 )
1128 .await
1129 .unwrap();
1130 let request = transport.next_incoming().await.unwrap();
1131 let AppServerIncoming::Request { id, method, .. } = request else {
1132 panic!("expected server request");
1133 };
1134 assert_eq!(method, "item/commandExecution/requestApproval");
1135 transport
1136 .respond(id, Ok(json!({"decision":"accept"})), Duration::from_secs(1))
1137 .await
1138 .unwrap();
1139 let response = BufReader::new(server_reader)
1140 .lines()
1141 .next_line()
1142 .await
1143 .unwrap()
1144 .unwrap();
1145 let response: Value = serde_json::from_str(&response).unwrap();
1146 assert_eq!(
1147 response,
1148 json!({"id":"approval-1","result":{"decision":"accept"}})
1149 );
1150 }
1151
1152 #[test]
1153 fn preserves_string_numeric_and_null_server_request_ids() {
1154 let cases = [
1155 (
1156 br#"{"id":"approval-7","method":"item/commandExecution/requestApproval"}"#
1157 .as_slice(),
1158 RpcId::String("approval-7".to_string()),
1159 ),
1160 (
1161 br#"{"id":42,"method":"item/fileChange/requestApproval"}"#.as_slice(),
1162 RpcId::Number("42".to_string()),
1163 ),
1164 (
1165 br#"{"id":null,"method":"tool/requestUserInput"}"#.as_slice(),
1166 RpcId::Null,
1167 ),
1168 ];
1169 for (frame, expected_id) in cases {
1170 match classify_frame(frame).unwrap() {
1171 WireMessage::Incoming(AppServerIncoming::Request { id, .. }) => {
1172 assert_eq!(id, expected_id)
1173 }
1174 _ => panic!("expected a server request"),
1175 }
1176 }
1177
1178 assert!(matches!(
1179 classify_frame(br#"{"result":{}}"#),
1180 Err(AppServerError::Protocol(message)) if message.contains("missing an id")
1181 ));
1182 assert!(matches!(
1183 classify_frame(br#"{"id":1,"result":{},"error":{"code":-1,"message":"bad"}}"#),
1184 Err(AppServerError::Protocol(message)) if message.contains("exactly one")
1185 ));
1186 }
1187
1188 #[tokio::test]
1189 async fn bounds_pending_requests_and_releases_cancelled_slots() {
1190 let limits = AppServerLimits {
1191 max_pending_requests: 1,
1192 ..AppServerLimits::default()
1193 };
1194 let (client_io, server_io) = duplex(4096);
1195 let (client_reader, client_writer) = tokio::io::split(client_io);
1196 let (server_reader, _server_writer) = tokio::io::split(server_io);
1197 let transport = Arc::new(AppServerTransport::new(
1198 client_reader,
1199 client_writer,
1200 limits,
1201 ));
1202 let (observed_tx, observed_rx) = oneshot::channel();
1203 let server = tokio::spawn(async move {
1204 let mut lines = BufReader::new(server_reader).lines();
1205 let _ = lines.next_line().await.unwrap().unwrap();
1206 let _ = observed_tx.send(());
1207 while lines.next_line().await.unwrap().is_some() {}
1208 });
1209 let first = {
1210 let transport = transport.clone();
1211 tokio::spawn(async move {
1212 transport
1213 .request("one", Value::Null, Duration::from_secs(5))
1214 .await
1215 })
1216 };
1217 observed_rx.await.unwrap();
1218
1219 assert_eq!(
1220 transport
1221 .request("two", Value::Null, Duration::from_secs(1))
1222 .await,
1223 Err(AppServerError::PendingRequestLimit(1))
1224 );
1225 first.abort();
1226 assert!(first.await.unwrap_err().is_cancelled());
1227 assert!(transport.shared.pending.lock().unwrap().is_empty());
1228
1229 assert_eq!(
1230 transport
1231 .request("timeout", Value::Null, Duration::from_millis(20))
1232 .await,
1233 Err(AppServerError::Timeout(Duration::from_millis(20)))
1234 );
1235 assert!(transport.shared.pending.lock().unwrap().is_empty());
1236 server.abort();
1237 }
1238
1239 #[tokio::test]
1240 async fn malformed_and_oversized_frames_fail_closed() {
1241 let limits = AppServerLimits {
1242 max_frame_bytes: 16,
1243 ..AppServerLimits::default()
1244 };
1245 let (client_io, mut server_io) = duplex(4096);
1246 let (client_reader, client_writer) = tokio::io::split(client_io);
1247 let transport = AppServerTransport::new(client_reader, client_writer, limits);
1248 server_io.write_all(b"not-json\n").await.unwrap();
1249 assert_eq!(transport.next_incoming().await, None);
1250 let error = transport
1251 .request("after-error", Value::Null, Duration::from_secs(1))
1252 .await
1253 .unwrap_err();
1254 assert!(matches!(error, AppServerError::Protocol(_)));
1255
1256 let (client_io, mut server_io) = duplex(4096);
1257 let (client_reader, client_writer) = tokio::io::split(client_io);
1258 let transport = AppServerTransport::new(client_reader, client_writer, limits);
1259 server_io
1260 .write_all(b"{\"way\":\"too long for bound\"}\n")
1261 .await
1262 .unwrap();
1263 assert_eq!(transport.next_incoming().await, None);
1264 let error = transport
1265 .request("after-error", Value::Null, Duration::from_secs(1))
1266 .await
1267 .unwrap_err();
1268 assert!(matches!(error, AppServerError::Protocol(_)));
1269 }
1270
1271 #[tokio::test]
1272 async fn initializes_and_gracefully_stops_a_fake_process() {
1273 let process = AppServerProcess::spawn(fake_command("happy"), AppServerLimits::default())
1274 .expect("spawn fake App Server");
1275 let response = process
1276 .transport
1277 .initialize(Duration::from_secs(2))
1278 .await
1279 .expect("initialize");
1280 assert_eq!(response["serverInfo"]["name"], "fake-app-server");
1281 assert!(process.transport.is_initialized());
1282 assert_eq!(
1283 process.transport.next_incoming().await,
1284 Some(AppServerIncoming::Notification {
1285 method: "server/ready".to_string(),
1286 params: serde_json::json!({ "fixture": true }),
1287 })
1288 );
1289
1290 let exit = process.shutdown(Duration::from_secs(2)).await.unwrap();
1291 match exit {
1292 AppServerExit::Clean { stderr_tail, .. } => {
1293 assert!(stderr_tail.contains("fake server started"));
1294 assert!(stderr_tail.contains("fake server observed EOF"));
1295 }
1296 other => panic!("expected clean exit, got {other:?}"),
1297 }
1298 }
1299
1300 #[tokio::test]
1310 async fn shutdown_can_force_kill_while_wait_for_exit_is_concurrently_awaiting() {
1311 let process = Arc::new(
1312 AppServerProcess::spawn(fake_command("ignores-shutdown"), AppServerLimits::default())
1313 .expect("spawn fake App Server"),
1314 );
1315 process
1316 .transport
1317 .initialize(Duration::from_secs(2))
1318 .await
1319 .expect("initialize");
1320
1321 let waiter_process = process.clone();
1324 let waiter = tokio::spawn(async move { waiter_process.wait_for_exit().await });
1325 tokio::time::sleep(Duration::from_millis(50)).await;
1326
1327 let shutdown_result = tokio::time::timeout(
1328 Duration::from_secs(5),
1329 process.shutdown(Duration::from_millis(200)),
1330 )
1331 .await
1332 .expect("shutdown() must not hang waiting on the same lock wait_for_exit() holds")
1333 .unwrap();
1334 assert!(
1335 matches!(shutdown_result, AppServerExit::ForcedAfterTimeout { .. }),
1336 "expected a forced kill after the 200ms grace period (this fixture ignores \
1337 graceful shutdown by design), got {shutdown_result:?}"
1338 );
1339
1340 let waiter_result = tokio::time::timeout(Duration::from_secs(5), waiter)
1348 .await
1349 .expect("the concurrent wait_for_exit() must resolve once shutdown() kills the process")
1350 .unwrap()
1351 .unwrap();
1352 assert!(
1353 matches!(waiter_result, AppServerExit::NonZero { .. }),
1354 "expected the killed process's real non-zero exit to surface here, got {waiter_result:?}"
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn classifies_exit_before_initialize_with_bounded_stderr() {
1360 let limits = AppServerLimits {
1361 max_stderr_bytes: 24,
1362 ..AppServerLimits::default()
1363 };
1364 let process = AppServerProcess::spawn(fake_command("exit-before-init"), limits)
1365 .expect("spawn fake App Server");
1366 let exit = process.wait_for_exit().await.unwrap();
1367 match exit {
1368 AppServerExit::BeforeInitialize { code, stderr_tail } => {
1369 assert_eq!(code, Some(23));
1370 assert!(stderr_tail.len() <= 24);
1371 assert!(stderr_tail.ends_with("before initialize\n"));
1372 }
1373 other => panic!("expected pre-initialize exit, got {other:?}"),
1374 }
1375 }
1376
1377 #[tokio::test]
1378 async fn classifies_post_initialize_nonzero_exit_and_unexpected_eof() {
1379 let process =
1380 AppServerProcess::spawn(fake_command("exit-after-init"), AppServerLimits::default())
1381 .expect("spawn fake App Server");
1382 process
1383 .transport
1384 .initialize(Duration::from_secs(2))
1385 .await
1386 .unwrap();
1387 match process.wait_for_exit().await.unwrap() {
1388 AppServerExit::NonZero { code, stderr_tail } => {
1389 assert_eq!(code, Some(27));
1390 assert!(stderr_tail.contains("failed after initialize"));
1391 }
1392 other => panic!("expected post-initialize failure, got {other:?}"),
1393 }
1394
1395 let process =
1396 AppServerProcess::spawn(fake_command("eof-after-init"), AppServerLimits::default())
1397 .expect("spawn fake App Server");
1398 process
1399 .transport
1400 .initialize(Duration::from_secs(2))
1401 .await
1402 .unwrap();
1403 assert!(matches!(
1404 process.wait_for_exit().await.unwrap(),
1405 AppServerExit::UnexpectedEof { .. }
1406 ));
1407 }
1408
1409 #[tokio::test]
1410 async fn classifies_malformed_stdout_as_protocol_violation() {
1411 let process = AppServerProcess::spawn(
1412 fake_command("protocol-violation"),
1413 AppServerLimits::default(),
1414 )
1415 .expect("spawn fake App Server");
1416 assert!(matches!(
1417 process.transport.initialize(Duration::from_secs(2)).await,
1418 Err(AppServerError::Protocol(_))
1419 ));
1420 match process.shutdown(Duration::from_secs(2)).await.unwrap() {
1421 AppServerExit::ProtocolViolation {
1422 message,
1423 stderr_tail,
1424 } => {
1425 assert!(message.contains("invalid JSON"));
1426 assert!(stderr_tail.contains("protocol violation"));
1427 }
1428 other => panic!("expected protocol violation, got {other:?}"),
1429 }
1430 }
1431
1432 fn fake_command(mode: &str) -> Command {
1433 let mut command = Command::new(fake_server_binary());
1434 command.env("AGENTMUX_FAKE_APP_SERVER_MODE", mode);
1435 command
1436 }
1437
1438 fn fake_server_binary() -> &'static std::path::PathBuf {
1439 crate::test_support::fake_app_server_binary()
1440 }
1441
1442 #[tokio::test]
1447 async fn write_outcome_tracker_stays_safe_to_retry_when_the_write_never_starts() {
1448 let (client_io, _server_io) = duplex(4096);
1449 let (client_reader, client_writer) = tokio::io::split(client_io);
1450 let transport =
1451 AppServerTransport::new(client_reader, client_writer, AppServerLimits::default());
1452 fail_transport(&transport.shared, AppServerError::Closed);
1453
1454 let tracker = WriteOutcomeTracker::new();
1455 let result = transport
1456 .write_value_tracked(json!({"x": 1}), &tracker)
1457 .await;
1458 assert!(result.is_err());
1459 assert!(tracker.safe_to_retry());
1460 }
1461
1462 #[tokio::test]
1468 async fn write_outcome_tracker_becomes_safe_to_retry_on_a_confirmed_write_failure() {
1469 let (client_io, server_io) = duplex(4096);
1470 drop(server_io);
1471 let (client_reader, client_writer) = tokio::io::split(client_io);
1472 let transport =
1473 AppServerTransport::new(client_reader, client_writer, AppServerLimits::default());
1474
1475 let tracker = WriteOutcomeTracker::new();
1476 let result = transport
1477 .write_value_tracked(json!({"x": 1}), &tracker)
1478 .await;
1479 assert!(result.is_err(), "the write should fail once the peer is gone");
1480 assert!(
1481 tracker.safe_to_retry(),
1482 "a confirmed write failure means the frame was genuinely never delivered"
1483 );
1484 }
1485
1486 #[tokio::test]
1494 async fn write_outcome_tracker_stays_unconfirmed_when_cancelled_after_queueing() {
1495 let (client_io, _server_io) = duplex(1);
1496 let (client_reader, client_writer) = tokio::io::split(client_io);
1497 let transport = Arc::new(AppServerTransport::new(
1498 client_reader,
1499 client_writer,
1500 AppServerLimits::default(),
1501 ));
1502 let tracker = Arc::new(WriteOutcomeTracker::new());
1503
1504 let task_transport = transport.clone();
1505 let task_tracker = tracker.clone();
1506 let handle = tokio::spawn(async move {
1507 task_transport
1508 .write_value_tracked(json!({"x": 1}), &task_tracker)
1509 .await
1510 });
1511
1512 for _ in 0..100 {
1513 if !tracker.safe_to_retry() {
1514 break;
1515 }
1516 tokio::task::yield_now().await;
1517 }
1518 assert!(
1519 !tracker.safe_to_retry(),
1520 "the frame should already be queued (unconfirmed) while the write is stuck"
1521 );
1522
1523 handle.abort();
1524 let _ = handle.await;
1525
1526 assert!(
1527 !tracker.safe_to_retry(),
1528 "a cancelled write whose outcome was never confirmed must stay unsafe to retry"
1529 );
1530 }
1531}