claude_codes/
client_raw_async.rs1use crate::cli::ClaudeCliBuilder;
6use crate::error::{Error, Result};
7use log::{debug, error};
8use serde::Serialize;
9use std::process::ExitStatus;
10use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
11use tokio::process::Child;
12
13const STDOUT_BUFFER_SIZE: usize = 10 * 1024 * 1024;
14
15pub struct RawAsyncClient {
16 child: Child,
17 writer: BufWriter<tokio::process::ChildStdin>,
18 reader: BufReader<tokio::process::ChildStdout>,
19 _stderr_drain: tokio::task::JoinHandle<()>,
20}
21
22impl RawAsyncClient {
23 pub async fn start_with(builder: ClaudeCliBuilder) -> Result<Self> {
24 crate::version::check_claude_version_async().await?;
25 Self::new(builder.spawn().await?)
26 }
27
28 pub fn new(mut child: Child) -> Result<Self> {
29 let stdin = child
30 .stdin
31 .take()
32 .ok_or_else(|| Error::Protocol("Failed to get stdin".to_string()))?;
33 let stdout = child
34 .stdout
35 .take()
36 .ok_or_else(|| Error::Protocol("Failed to get stdout".to_string()))?;
37 let stderr = child
38 .stderr
39 .take()
40 .ok_or_else(|| Error::Protocol("Failed to get stderr".to_string()))?;
41 let stderr_drain = tokio::spawn(async move {
42 let mut stderr = BufReader::new(stderr);
43 let mut line = String::new();
44 loop {
45 line.clear();
46 match stderr.read_line(&mut line).await {
47 Ok(0) | Err(_) => break,
48 Ok(_) => debug!("[RAW CLIENT STDERR] {}", line.trim_end()),
49 }
50 }
51 });
52
53 Ok(Self {
54 child,
55 writer: BufWriter::new(stdin),
56 reader: BufReader::with_capacity(STDOUT_BUFFER_SIZE, stdout),
57 _stderr_drain: stderr_drain,
58 })
59 }
60
61 pub async fn send<T: Serialize>(&mut self, message: &T) -> Result<()> {
63 let line = serde_json::to_string(message).map_err(Error::Json)?;
64 self.write_line(&line).await
65 }
66
67 async fn write_line(&mut self, line: &str) -> Result<()> {
68 let line = single_line(line)?;
69 debug!("[RAW CLIENT] Sending {} bytes", line.len());
70 self.writer
71 .write_all(line.as_bytes())
72 .await
73 .map_err(Error::Io)?;
74 self.writer.write_all(b"\n").await.map_err(Error::Io)?;
75 self.writer.flush().await.map_err(Error::Io)
76 }
77
78 pub async fn next_line(&mut self) -> Result<Option<String>> {
80 let mut line = String::new();
81 loop {
82 line.clear();
83 if self.reader.read_line(&mut line).await.map_err(Error::Io)? == 0 {
84 return Ok(None);
85 }
86 remove_line_ending(&mut line);
87 if line.trim().is_empty() {
88 continue;
89 }
90 debug!("[RAW CLIENT] Received {} bytes", line.len());
91 return Ok(Some(line));
92 }
93 }
94
95 pub fn pid(&self) -> Option<u32> {
96 self.child.id()
97 }
98
99 pub fn is_alive(&mut self) -> bool {
100 self.child.try_wait().ok().flatten().is_none()
101 }
102
103 pub async fn wait_for_exit(&mut self) -> Result<ExitStatus> {
104 self.child.wait().await.map_err(Error::Io)
105 }
106
107 pub async fn shutdown(mut self) -> Result<()> {
108 self.child.kill().await.map_err(Error::Io)
109 }
110}
111
112impl Drop for RawAsyncClient {
113 fn drop(&mut self) {
114 if self.is_alive() {
115 if let Err(error) = self.child.start_kill() {
116 error!("Failed to kill raw Claude process on drop: {error}");
117 }
118 }
119 }
120}
121
122fn single_line(line: &str) -> Result<&str> {
123 let line = line.trim_end_matches(['\r', '\n']);
124 if line.contains(['\r', '\n']) {
125 return Err(Error::Protocol(
126 "raw Claude frame contains an embedded line break".to_string(),
127 ));
128 }
129 Ok(line)
130}
131
132fn remove_line_ending(line: &mut String) {
133 if line.ends_with('\n') {
134 line.pop();
135 if line.ends_with('\r') {
136 line.pop();
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn accepts_one_frame_with_a_line_ending() {
147 assert_eq!(
148 single_line("{\"type\":\"user\"}\r\n").unwrap(),
149 "{\"type\":\"user\"}"
150 );
151 }
152
153 #[test]
154 fn rejects_multiple_frames() {
155 assert!(single_line("{\"type\":\"user\"}\n{}").is_err());
156 }
157}