Download codex-rs/exec-server/src/connection.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 36.9 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server/src/connection.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/exec-server/src/connection.rs
-
curl -L -o connection.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server/src/connection.rs
36.9 kB
| use std::process::Stdio; | |
| use std::sync::Arc; | |
| use std::sync::atomic::AtomicBool; | |
| use std::sync::atomic::Ordering; | |
| use std::time::Duration; | |
| use std::time::Instant; | |
| use axum::extract::ws::Message as AxumWebSocketMessage; | |
| use axum::extract::ws::WebSocket as AxumWebSocket; | |
| use codex_exec_server_protocol::JSONRPCMessage; | |
| use codex_exec_server_protocol::JSONRPCRequest; | |
| use futures::Sink; | |
| use futures::SinkExt; | |
| use futures::Stream; | |
| use futures::StreamExt; | |
| use tokio::io::AsyncRead; | |
| use tokio::io::AsyncWrite; | |
| use tokio::process::Child; | |
| use tokio::sync::mpsc; | |
| use tokio::sync::watch; | |
| use tokio::time::timeout; | |
| use tokio_tungstenite::WebSocketStream; | |
| use tokio_tungstenite::tungstenite::Message; | |
| use tracing::debug; | |
| use tracing::warn; | |
| use tokio::io::AsyncBufReadExt; | |
| use tokio::io::AsyncReadExt; | |
| use tokio::io::AsyncWriteExt; | |
| use tokio::io::BufReader; | |
| use tokio::io::BufWriter; | |
| pub(crate) const CHANNEL_CAPACITY: usize = 128; | |
| // Match the existing serialized JSON-RPC message ceiling used by Noise and | |
| // WebSocket transports so stdio has the same per-message bound. | |
| const MAX_STDIO_JSONRPC_MESSAGE_LEN: usize = 64 * 1024 * 1024; | |
| const STDIO_TERMINATION_GRACE_PERIOD: Duration = Duration::from_secs(2); | |
| pub(crate) const WEBSOCKET_KEEPALIVE_INTERVAL: Duration = Duration::from_millis(25); | |
| pub(crate) const WEBSOCKET_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30); | |
| pub(crate) enum JsonRpcConnectionEvent { | |
| Message(JSONRPCMessage), | |
| QueuedRequest { | |
| request: JSONRPCRequest, | |
| request_span: tracing::Span, | |
| queued_at: Instant, | |
| }, | |
| MalformedMessage { | |
| reason: String, | |
| }, | |
| Disconnected { | |
| reason: Option<String>, | |
| }, | |
| } | |
| impl JsonRpcConnectionEvent { | |
| pub(crate) fn message(message: JSONRPCMessage) -> Self { | |
| let JSONRPCMessage::Request(request) = message else { | |
| return Self::Message(message); | |
| }; | |
| let queued_at = Instant::now(); | |
| let request_span = tracing::info_span!( | |
| "codex.exec_server.request", | |
| otel.kind = "server", | |
| otel.name = "unknown", | |
| method = request.method.as_str(), | |
| result = tracing::field::Empty, | |
| ); | |
| if let Some(trace) = &request.trace | |
| && !codex_otel::set_parent_from_w3c_trace_context(&request_span, trace) | |
| { | |
| warn!( | |
| method = request.method.as_str(), | |
| "ignoring invalid inbound exec-server trace carrier" | |
| ); | |
| } | |
| Self::QueuedRequest { | |
| request, | |
| request_span, | |
| queued_at, | |
| } | |
| } | |
| } | |
| pub(crate) enum JsonRpcTransport { | |
| // Plain means no child process; transport bytes may still be encrypted. | |
| Plain, | |
| Stdio { transport: StdioTransport }, | |
| } | |
| impl JsonRpcTransport { | |
| fn from_child_process(child_process: Child) -> Self { | |
| Self::Stdio { | |
| transport: StdioTransport::spawn(child_process), | |
| } | |
| } | |
| pub(crate) fn terminate(&self) { | |
| match self { | |
| Self::Plain => {} | |
| Self::Stdio { transport } => transport.terminate(), | |
| } | |
| } | |
| } | |
| pub(crate) struct StdioTransport { | |
| handle: Arc<StdioTransportHandle>, | |
| } | |
| struct StdioTransportHandle { | |
| terminate_tx: watch::Sender<bool>, | |
| terminate_requested: AtomicBool, | |
| } | |
| impl StdioTransport { | |
| fn spawn(child_process: Child) -> Self { | |
| let (terminate_tx, terminate_rx) = watch::channel(false); | |
| let handle = Arc::new(StdioTransportHandle { | |
| terminate_tx, | |
| terminate_requested: AtomicBool::new(false), | |
| }); | |
| spawn_stdio_child_supervisor(child_process, terminate_rx); | |
| Self { handle } | |
| } | |
| fn terminate(&self) { | |
| self.handle.terminate(); | |
| } | |
| } | |
| impl StdioTransportHandle { | |
| fn terminate(&self) { | |
| if !self.terminate_requested.swap(true, Ordering::AcqRel) { | |
| let _ = self.terminate_tx.send(true); | |
| } | |
| } | |
| } | |
| impl Drop for StdioTransportHandle { | |
| fn drop(&mut self) { | |
| self.terminate(); | |
| } | |
| } | |
| fn spawn_stdio_child_supervisor(mut child_process: Child, mut terminate_rx: watch::Receiver<bool>) { | |
| let process_group_id = child_process.id(); | |
| tokio::spawn(async move { | |
| tokio::select! { | |
| result = child_process.wait() => { | |
| log_stdio_child_wait_result(result); | |
| kill_process_tree(&mut child_process, process_group_id); | |
| } | |
| () = wait_for_stdio_termination(&mut terminate_rx) => { | |
| terminate_stdio_child(&mut child_process, process_group_id).await; | |
| } | |
| } | |
| }); | |
| } | |
| async fn wait_for_stdio_termination(terminate_rx: &mut watch::Receiver<bool>) { | |
| loop { | |
| if *terminate_rx.borrow() { | |
| return; | |
| } | |
| if terminate_rx.changed().await.is_err() { | |
| return; | |
| } | |
| } | |
| } | |
| async fn terminate_stdio_child(child_process: &mut Child, process_group_id: Option<u32>) { | |
| terminate_process_tree(child_process, process_group_id); | |
| match timeout(STDIO_TERMINATION_GRACE_PERIOD, child_process.wait()).await { | |
| Ok(result) => { | |
| log_stdio_child_wait_result(result); | |
| } | |
| Err(_) => { | |
| kill_process_tree(child_process, process_group_id); | |
| log_stdio_child_wait_result(child_process.wait().await); | |
| } | |
| } | |
| } | |
| fn terminate_process_tree(child_process: &mut Child, process_group_id: Option<u32>) { | |
| let Some(process_group_id) = process_group_id else { | |
| kill_direct_child(child_process, "terminate"); | |
| return; | |
| }; | |
| if let Err(err) = codex_utils_pty::process_group::terminate_process_group(process_group_id) { | |
| warn!("failed to terminate exec-server stdio process group {process_group_id}: {err}"); | |
| kill_direct_child(child_process, "terminate"); | |
| } | |
| if !kill_windows_process_tree(process_group_id) { | |
| kill_direct_child(child_process, "terminate"); | |
| } | |
| { | |
| let _ = process_group_id; | |
| kill_direct_child(child_process, "terminate"); | |
| } | |
| } | |
| fn kill_process_tree(child_process: &mut Child, process_group_id: Option<u32>) { | |
| let Some(process_group_id) = process_group_id else { | |
| kill_direct_child(child_process, "kill"); | |
| return; | |
| }; | |
| if let Err(err) = codex_utils_pty::process_group::kill_process_group(process_group_id) { | |
| warn!("failed to kill exec-server stdio process group {process_group_id}: {err}"); | |
| } | |
| if !kill_windows_process_tree(process_group_id) { | |
| kill_direct_child(child_process, "kill"); | |
| } | |
| { | |
| let _ = process_group_id; | |
| kill_direct_child(child_process, "kill"); | |
| } | |
| } | |
| fn kill_direct_child(child_process: &mut Child, action: &str) { | |
| if let Err(err) = child_process.start_kill() { | |
| debug!("failed to {action} exec-server stdio child: {err}"); | |
| } | |
| } | |
| fn kill_windows_process_tree(pid: u32) -> bool { | |
| let pid = pid.to_string(); | |
| match std::process::Command::new("taskkill") | |
| .args(["/PID", pid.as_str(), "/T", "/F"]) | |
| .stdin(Stdio::null()) | |
| .stdout(Stdio::null()) | |
| .stderr(Stdio::null()) | |
| .status() | |
| { | |
| Ok(status) => status.success(), | |
| Err(err) => { | |
| warn!("failed to run taskkill for exec-server stdio process tree {pid}: {err}"); | |
| false | |
| } | |
| } | |
| } | |
| fn log_stdio_child_wait_result(result: std::io::Result<std::process::ExitStatus>) { | |
| if let Err(err) = result { | |
| debug!("failed to wait for exec-server stdio child: {err}"); | |
| } | |
| } | |
| pub(crate) struct JsonRpcConnection { | |
| pub(crate) outgoing_tx: mpsc::Sender<JSONRPCMessage>, | |
| pub(crate) incoming_rx: mpsc::Receiver<JsonRpcConnectionEvent>, | |
| pub(crate) disconnected_rx: watch::Receiver<bool>, | |
| pub(crate) task_handles: Vec<tokio::task::JoinHandle<()>>, | |
| pub(crate) transport: JsonRpcTransport, | |
| } | |
| impl JsonRpcConnection { | |
| pub(crate) fn from_stdio<R, W>(reader: R, writer: W, connection_label: String) -> Self | |
| where | |
| R: AsyncRead + Unpin + Send + 'static, | |
| W: AsyncWrite + Unpin + Send + 'static, | |
| { | |
| Self::from_stdio_with_max_message_len( | |
| reader, | |
| writer, | |
| connection_label, | |
| MAX_STDIO_JSONRPC_MESSAGE_LEN, | |
| ) | |
| } | |
| fn from_stdio_with_max_message_len<R, W>( | |
| reader: R, | |
| writer: W, | |
| connection_label: String, | |
| max_message_len: usize, | |
| ) -> Self | |
| where | |
| R: AsyncRead + Unpin + Send + 'static, | |
| W: AsyncWrite + Unpin + Send + 'static, | |
| { | |
| let (outgoing_tx, mut outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY); | |
| let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY); | |
| let (disconnected_tx, disconnected_rx) = watch::channel(false); | |
| let reader_label = connection_label.clone(); | |
| let incoming_tx_for_reader = incoming_tx.clone(); | |
| let disconnected_tx_for_reader = disconnected_tx.clone(); | |
| // Read one byte past the payload limit so an unterminated oversized | |
| // message fails promptly. A trailing CR gets one more byte of lookahead | |
| // because it may be the first half of a valid CRLF terminator. | |
| let read_limit = u64::try_from(max_message_len.saturating_add(1)).unwrap_or(u64::MAX); | |
| let reader_task = tokio::spawn(async move { | |
| let mut reader = BufReader::new(reader); | |
| let mut line = String::new(); | |
| loop { | |
| line.clear(); | |
| let read_result = (&mut reader).take(read_limit).read_line(&mut line).await; | |
| match read_result { | |
| Ok(0) => { | |
| send_disconnected( | |
| &incoming_tx_for_reader, | |
| &disconnected_tx_for_reader, | |
| /*reason*/ None, | |
| ) | |
| .await; | |
| break; | |
| } | |
| Ok(_) => { | |
| if line.ends_with('\n') { | |
| line.pop(); | |
| if line.ends_with('\r') { | |
| line.pop(); | |
| } | |
| } else if line.len() > max_message_len && line.ends_with('\r') { | |
| match reader.read_u8().await { | |
| Ok(b'\n') => { | |
| line.pop(); | |
| } | |
| Ok(_) => {} | |
| Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {} | |
| Err(err) => { | |
| send_disconnected( | |
| &incoming_tx_for_reader, | |
| &disconnected_tx_for_reader, | |
| Some(format!( | |
| "failed to read JSON-RPC message from {reader_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| break; | |
| } | |
| } | |
| } | |
| if line.len() > max_message_len { | |
| send_disconnected( | |
| &incoming_tx_for_reader, | |
| &disconnected_tx_for_reader, | |
| Some(format!( | |
| "JSON-RPC message from {reader_label} exceeds maximum length of {max_message_len} bytes" | |
| )), | |
| ) | |
| .await; | |
| break; | |
| } | |
| if line.trim().is_empty() { | |
| continue; | |
| } | |
| match serde_json::from_str::<JSONRPCMessage>(&line) { | |
| Ok(message) => { | |
| if incoming_tx_for_reader | |
| .send(JsonRpcConnectionEvent::message(message)) | |
| .await | |
| .is_err() | |
| { | |
| break; | |
| } | |
| } | |
| Err(err) => { | |
| send_malformed_message( | |
| &incoming_tx_for_reader, | |
| Some(format!( | |
| "failed to parse JSON-RPC message from {reader_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| } | |
| } | |
| } | |
| Err(err) => { | |
| send_disconnected( | |
| &incoming_tx_for_reader, | |
| &disconnected_tx_for_reader, | |
| Some(format!( | |
| "failed to read JSON-RPC message from {reader_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| break; | |
| } | |
| } | |
| } | |
| }); | |
| let writer_task = tokio::spawn(async move { | |
| let mut writer = BufWriter::new(writer); | |
| while let Some(message) = outgoing_rx.recv().await { | |
| if let Err(err) = write_jsonrpc_line_message(&mut writer, &message).await { | |
| send_disconnected( | |
| &incoming_tx, | |
| &disconnected_tx, | |
| Some(format!( | |
| "failed to write JSON-RPC message to {connection_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| break; | |
| } | |
| } | |
| }); | |
| Self { | |
| outgoing_tx, | |
| incoming_rx, | |
| disconnected_rx, | |
| task_handles: vec![reader_task, writer_task], | |
| transport: JsonRpcTransport::Plain, | |
| } | |
| } | |
| pub(crate) fn from_websocket<T, E>(stream: T, connection_label: String) -> Self | |
| where | |
| T: Sink<Message, Error = E> + Stream<Item = Result<Message, E>> + Unpin + Send + 'static, | |
| E: std::fmt::Display + Send + 'static, | |
| { | |
| Self::from_websocket_stream(stream, connection_label, /*ping_interval*/ None) | |
| } | |
| pub(crate) fn from_axum_websocket(stream: AxumWebSocket, connection_label: String) -> Self { | |
| Self::from_websocket_stream(stream, connection_label, Some(WEBSOCKET_KEEPALIVE_INTERVAL)) | |
| } | |
| fn from_websocket_stream<T, M, E>( | |
| mut websocket: T, | |
| connection_label: String, | |
| ping_interval: Option<Duration>, | |
| ) -> Self | |
| where | |
| T: Sink<M, Error = E> + Stream<Item = Result<M, E>> + Unpin + Send + 'static, | |
| M: JsonRpcWebSocketMessage, | |
| E: std::fmt::Display + Send + 'static, | |
| { | |
| let (outgoing_tx, mut outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY); | |
| let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY); | |
| let (disconnected_tx, disconnected_rx) = watch::channel(false); | |
| let websocket_task = tokio::spawn(async move { | |
| let mut ping_interval = ping_interval.map(|ping_interval| { | |
| let mut interval = tokio::time::interval_at( | |
| tokio::time::Instant::now() + ping_interval, | |
| ping_interval, | |
| ); | |
| interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); | |
| interval | |
| }); | |
| loop { | |
| tokio::select! { | |
| maybe_message = outgoing_rx.recv() => { | |
| let Some(message) = maybe_message else { | |
| break; | |
| }; | |
| if let Err(reason) = send_websocket_jsonrpc_message( | |
| &mut websocket, | |
| &connection_label, | |
| &message, | |
| ) | |
| .await | |
| { | |
| send_disconnected(&incoming_tx, &disconnected_tx, Some(reason)).await; | |
| break; | |
| } | |
| } | |
| _ = async { | |
| match ping_interval.as_mut() { | |
| Some(interval) => interval.tick().await, | |
| None => std::future::pending().await, | |
| } | |
| } => { | |
| if let Err(err) = websocket.send(M::ping()).await { | |
| send_disconnected( | |
| &incoming_tx, | |
| &disconnected_tx, | |
| Some(format!( | |
| "failed to write websocket ping to {connection_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| break; | |
| } | |
| } | |
| incoming_message = websocket.next() => { | |
| match incoming_message { | |
| Some(Ok(message)) => match message.parse_jsonrpc_frame() { | |
| Ok(JsonRpcWebSocketFrame::Message(message)) => { | |
| if incoming_tx | |
| .send(JsonRpcConnectionEvent::message(message)) | |
| .await | |
| .is_err() | |
| { | |
| break; | |
| } | |
| } | |
| Ok(JsonRpcWebSocketFrame::Close) => { | |
| send_disconnected( | |
| &incoming_tx, | |
| &disconnected_tx, | |
| /*reason*/ None, | |
| ) | |
| .await; | |
| break; | |
| } | |
| Ok(JsonRpcWebSocketFrame::Ignore) => {} | |
| Err(err) => { | |
| send_malformed_message( | |
| &incoming_tx, | |
| Some(format!( | |
| "failed to parse websocket JSON-RPC message from {connection_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| } | |
| }, | |
| Some(Err(err)) => { | |
| send_disconnected( | |
| &incoming_tx, | |
| &disconnected_tx, | |
| Some(format!( | |
| "failed to read websocket JSON-RPC message from {connection_label}: {err}" | |
| )), | |
| ) | |
| .await; | |
| break; | |
| } | |
| None => { | |
| send_disconnected( | |
| &incoming_tx, | |
| &disconnected_tx, | |
| /*reason*/ None, | |
| ) | |
| .await; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| Self { | |
| outgoing_tx, | |
| incoming_rx, | |
| disconnected_rx, | |
| task_handles: vec![websocket_task], | |
| transport: JsonRpcTransport::Plain, | |
| } | |
| } | |
| pub(crate) fn with_child_process(mut self, child_process: Child) -> Self { | |
| self.transport = JsonRpcTransport::from_child_process(child_process); | |
| self | |
| } | |
| } | |
| enum JsonRpcWebSocketFrame { | |
| Message(JSONRPCMessage), | |
| Close, | |
| Ignore, | |
| } | |
| trait JsonRpcWebSocketMessage: Send + 'static { | |
| fn parse_jsonrpc_frame(self) -> Result<JsonRpcWebSocketFrame, serde_json::Error>; | |
| fn from_text(text: String) -> Self; | |
| fn ping() -> Self; | |
| } | |
| impl JsonRpcWebSocketMessage for Message { | |
| fn parse_jsonrpc_frame(self) -> Result<JsonRpcWebSocketFrame, serde_json::Error> { | |
| match self { | |
| Message::Text(text) => { | |
| serde_json::from_str(text.as_ref()).map(JsonRpcWebSocketFrame::Message) | |
| } | |
| Message::Binary(bytes) => { | |
| serde_json::from_slice(bytes.as_ref()).map(JsonRpcWebSocketFrame::Message) | |
| } | |
| Message::Close(_) => Ok(JsonRpcWebSocketFrame::Close), | |
| Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => { | |
| Ok(JsonRpcWebSocketFrame::Ignore) | |
| } | |
| } | |
| } | |
| fn from_text(text: String) -> Self { | |
| Self::Text(text.into()) | |
| } | |
| fn ping() -> Self { | |
| Self::Ping(Vec::new().into()) | |
| } | |
| } | |
| impl JsonRpcWebSocketMessage for AxumWebSocketMessage { | |
| fn parse_jsonrpc_frame(self) -> Result<JsonRpcWebSocketFrame, serde_json::Error> { | |
| match self { | |
| AxumWebSocketMessage::Text(text) => { | |
| serde_json::from_str(text.as_ref()).map(JsonRpcWebSocketFrame::Message) | |
| } | |
| AxumWebSocketMessage::Binary(bytes) => { | |
| serde_json::from_slice(bytes.as_ref()).map(JsonRpcWebSocketFrame::Message) | |
| } | |
| AxumWebSocketMessage::Close(_) => Ok(JsonRpcWebSocketFrame::Close), | |
| AxumWebSocketMessage::Ping(_) | AxumWebSocketMessage::Pong(_) => { | |
| Ok(JsonRpcWebSocketFrame::Ignore) | |
| } | |
| } | |
| } | |
| fn from_text(text: String) -> Self { | |
| Self::Text(text.into()) | |
| } | |
| fn ping() -> Self { | |
| Self::Ping(Vec::new().into()) | |
| } | |
| } | |
| async fn send_disconnected( | |
| incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>, | |
| disconnected_tx: &watch::Sender<bool>, | |
| reason: Option<String>, | |
| ) { | |
| let _ = disconnected_tx.send(true); | |
| let _ = incoming_tx | |
| .send(JsonRpcConnectionEvent::Disconnected { reason }) | |
| .await; | |
| } | |
| async fn send_malformed_message( | |
| incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>, | |
| reason: Option<String>, | |
| ) { | |
| let _ = incoming_tx | |
| .send(JsonRpcConnectionEvent::MalformedMessage { | |
| reason: reason.unwrap_or_else(|| "malformed JSON-RPC message".to_string()), | |
| }) | |
| .await; | |
| } | |
| async fn write_jsonrpc_line_message<W>( | |
| writer: &mut BufWriter<W>, | |
| message: &JSONRPCMessage, | |
| ) -> std::io::Result<()> | |
| where | |
| W: AsyncWrite + Unpin, | |
| { | |
| let encoded = | |
| serialize_jsonrpc_message(message).map_err(|err| std::io::Error::other(err.to_string()))?; | |
| writer.write_all(encoded.as_bytes()).await?; | |
| writer.write_all(b"\n").await?; | |
| writer.flush().await | |
| } | |
| async fn send_websocket_jsonrpc_message<W, M, E>( | |
| websocket_writer: &mut W, | |
| connection_label: &str, | |
| message: &JSONRPCMessage, | |
| ) -> Result<(), String> | |
| where | |
| W: Sink<M, Error = E> + Unpin, | |
| M: JsonRpcWebSocketMessage, | |
| E: std::fmt::Display, | |
| { | |
| match serialize_jsonrpc_message(message) { | |
| Ok(encoded) => websocket_writer | |
| .send(M::from_text(encoded)) | |
| .await | |
| .map_err(|err| { | |
| format!("failed to write websocket JSON-RPC message to {connection_label}: {err}") | |
| }), | |
| Err(err) => Err(format!( | |
| "failed to serialize JSON-RPC message for {connection_label}: {err}" | |
| )), | |
| } | |
| } | |
| fn serialize_jsonrpc_message(message: &JSONRPCMessage) -> Result<String, serde_json::Error> { | |
| serde_json::to_string(message) | |
| } | |
| mod tests { | |
| use std::pin::Pin; | |
| use std::sync::Arc; | |
| use std::sync::atomic::AtomicBool; | |
| use std::sync::atomic::Ordering; | |
| use std::task::Context; | |
| use std::task::Poll; | |
| use codex_exec_server_protocol::JSONRPCRequest; | |
| use codex_exec_server_protocol::RequestId; | |
| use futures::channel::mpsc as futures_mpsc; | |
| use futures::task::AtomicWaker; | |
| use pretty_assertions::assert_eq; | |
| use tokio::net::TcpListener; | |
| use tokio::time::timeout; | |
| use tokio_tungstenite::accept_async; | |
| use tokio_tungstenite::connect_async; | |
| use super::*; | |
| async fn stdio_connection_accepts_message_at_size_limit() -> anyhow::Result<()> { | |
| let message = test_jsonrpc_message(); | |
| let encoded = serde_json::to_string(&message)?; | |
| let max_message_len = encoded.len(); | |
| for line_ending in [b"\n".as_slice(), b"\r\n".as_slice()] { | |
| let (reader, mut peer) = | |
| tokio::io::duplex(max_message_len.saturating_add(line_ending.len())); | |
| let mut connection = JsonRpcConnection::from_stdio_with_max_message_len( | |
| reader, | |
| tokio::io::sink(), | |
| "test stdio peer".to_string(), | |
| max_message_len, | |
| ); | |
| peer.write_all(encoded.as_bytes()).await?; | |
| peer.write_all(line_ending).await?; | |
| let event = timeout(Duration::from_secs(1), connection.incoming_rx.recv()) | |
| .await? | |
| .expect("stdio connection should report the message"); | |
| match event { | |
| JsonRpcConnectionEvent::QueuedRequest { request, .. } => { | |
| assert_eq!(JSONRPCMessage::Request(request), message) | |
| } | |
| event => anyhow::bail!("expected JSON-RPC message, got {event:?}"), | |
| } | |
| drop(peer); | |
| drop(connection); | |
| } | |
| Ok(()) | |
| } | |
| async fn stdio_connection_rejects_overlong_unterminated_message() -> anyhow::Result<()> { | |
| let max_message_len: usize = 32; | |
| let (reader, mut peer) = tokio::io::duplex(max_message_len.saturating_add(1)); | |
| let mut connection = JsonRpcConnection::from_stdio_with_max_message_len( | |
| reader, | |
| tokio::io::sink(), | |
| "hostile stdio peer".to_string(), | |
| max_message_len, | |
| ); | |
| let overlong_message = vec![b'x'; max_message_len + 1]; | |
| peer.write_all(&overlong_message).await?; | |
| let event = timeout(Duration::from_secs(1), connection.incoming_rx.recv()) | |
| .await? | |
| .expect("stdio connection should report the framing violation"); | |
| match event { | |
| JsonRpcConnectionEvent::Disconnected { reason } => assert_eq!( | |
| reason, | |
| Some( | |
| "JSON-RPC message from hostile stdio peer exceeds maximum length of 32 bytes" | |
| .to_string() | |
| ) | |
| ), | |
| event => anyhow::bail!("expected stdio disconnect, got {event:?}"), | |
| } | |
| drop(peer); | |
| drop(connection); | |
| Ok(()) | |
| } | |
| async fn websocket_connection_sends_configured_ping() -> anyhow::Result<()> { | |
| let (client_websocket, mut server_websocket) = websocket_pair().await?; | |
| let connection = JsonRpcConnection::from_websocket_stream( | |
| client_websocket, | |
| "test".into(), | |
| Some(WEBSOCKET_KEEPALIVE_INTERVAL), | |
| ); | |
| let message = timeout(Duration::from_secs(1), server_websocket.next()) | |
| .await? | |
| .expect("websocket should stay open")?; | |
| assert!(matches!(message, Message::Ping(_))); | |
| drop(connection); | |
| Ok(()) | |
| } | |
| async fn websocket_connection_ignores_server_pong() -> anyhow::Result<()> { | |
| let (client_websocket, mut server_websocket) = websocket_pair().await?; | |
| let mut connection = JsonRpcConnection::from_websocket(client_websocket, "test".into()); | |
| server_websocket | |
| .send(Message::Pong(b"check".to_vec().into())) | |
| .await?; | |
| assert!( | |
| timeout(Duration::from_millis(50), connection.incoming_rx.recv()) | |
| .await | |
| .is_err() | |
| ); | |
| drop(connection); | |
| Ok(()) | |
| } | |
| async fn websocket_connection_reports_server_close() -> anyhow::Result<()> { | |
| let (client_websocket, mut server_websocket) = websocket_pair().await?; | |
| let mut connection = JsonRpcConnection::from_websocket(client_websocket, "test".into()); | |
| server_websocket.close(None).await?; | |
| assert!(matches!( | |
| timeout(Duration::from_secs(1), connection.incoming_rx.recv()).await?, | |
| Some(JsonRpcConnectionEvent::Disconnected { reason: None }) | |
| )); | |
| drop(connection); | |
| Ok(()) | |
| } | |
| async fn websocket_connection_accepts_binary_jsonrpc_message() -> anyhow::Result<()> { | |
| let (client_websocket, mut server_websocket) = websocket_pair().await?; | |
| let mut connection = JsonRpcConnection::from_websocket(client_websocket, "test".into()); | |
| let message = JSONRPCMessage::Request(JSONRPCRequest { | |
| id: RequestId::Integer(1), | |
| method: "test".to_string(), | |
| params: None, | |
| trace: None, | |
| }); | |
| server_websocket | |
| .send(Message::Binary(serde_json::to_vec(&message)?.into())) | |
| .await?; | |
| let Some(JsonRpcConnectionEvent::QueuedRequest { request, .. }) = | |
| timeout(Duration::from_secs(1), connection.incoming_rx.recv()).await? | |
| else { | |
| anyhow::bail!("expected a queued JSON-RPC request"); | |
| }; | |
| assert_eq!(JSONRPCMessage::Request(request), message); | |
| drop(connection); | |
| Ok(()) | |
| } | |
| async fn websocket_connection_keeps_outbound_message_while_send_is_backpressured() | |
| -> anyhow::Result<()> { | |
| let (websocket, control, mut outbound_rx) = | |
| ControlledWebSocket::new(/*write_ready*/ false); | |
| let mut connection = JsonRpcConnection::from_websocket_stream( | |
| websocket, | |
| "test".into(), | |
| /*ping_interval*/ None, | |
| ); | |
| let message = test_jsonrpc_message(); | |
| connection.outgoing_tx.send(message.clone()).await?; | |
| control.wait_for_blocked_write().await?; | |
| control.send_inbound(Message::Pong(b"check".to_vec().into()))?; | |
| assert!( | |
| timeout(Duration::from_millis(50), connection.incoming_rx.recv()) | |
| .await | |
| .is_err() | |
| ); | |
| control.set_write_ready(); | |
| assert!(matches!( | |
| timeout(Duration::from_secs(1), outbound_rx.next()).await?, | |
| Some(Message::Text(text)) if serde_json::from_str::<JSONRPCMessage>(&text)? == message | |
| )); | |
| drop(connection); | |
| Ok(()) | |
| } | |
| async fn websocket_pair() -> anyhow::Result<( | |
| WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>, | |
| WebSocketStream<tokio::net::TcpStream>, | |
| )> { | |
| let listener = TcpListener::bind("127.0.0.1:0").await?; | |
| let websocket_url = format!("ws://{}", listener.local_addr()?); | |
| let server_task = tokio::spawn(async move { | |
| let (stream, _) = listener.accept().await?; | |
| accept_async(stream).await.map_err(anyhow::Error::from) | |
| }); | |
| let (client_websocket, _) = connect_async(websocket_url).await?; | |
| let server_websocket = server_task.await??; | |
| Ok((client_websocket, server_websocket)) | |
| } | |
| fn test_jsonrpc_message() -> JSONRPCMessage { | |
| JSONRPCMessage::Request(JSONRPCRequest { | |
| id: RequestId::Integer(1), | |
| method: "test".to_string(), | |
| params: None, | |
| trace: None, | |
| }) | |
| } | |
| struct ControlledWebSocket { | |
| inbound_rx: futures_mpsc::UnboundedReceiver<Result<Message, std::convert::Infallible>>, | |
| outbound_tx: futures_mpsc::UnboundedSender<Message>, | |
| write_ready: Arc<AtomicBool>, | |
| write_blocked: Arc<AtomicBool>, | |
| write_blocked_waker: Arc<AtomicWaker>, | |
| write_waker: Arc<AtomicWaker>, | |
| } | |
| struct ControlledWebSocketHandle { | |
| inbound_tx: futures_mpsc::UnboundedSender<Result<Message, std::convert::Infallible>>, | |
| write_ready: Arc<AtomicBool>, | |
| write_blocked: Arc<AtomicBool>, | |
| write_blocked_waker: Arc<AtomicWaker>, | |
| write_waker: Arc<AtomicWaker>, | |
| } | |
| impl ControlledWebSocket { | |
| fn new( | |
| write_ready: bool, | |
| ) -> ( | |
| Self, | |
| ControlledWebSocketHandle, | |
| futures_mpsc::UnboundedReceiver<Message>, | |
| ) { | |
| let (inbound_tx, inbound_rx) = futures_mpsc::unbounded(); | |
| let (outbound_tx, outbound_rx) = futures_mpsc::unbounded(); | |
| let write_ready = Arc::new(AtomicBool::new(write_ready)); | |
| let write_blocked = Arc::new(AtomicBool::new(false)); | |
| let write_blocked_waker = Arc::new(AtomicWaker::new()); | |
| let write_waker = Arc::new(AtomicWaker::new()); | |
| ( | |
| Self { | |
| inbound_rx, | |
| outbound_tx, | |
| write_ready: Arc::clone(&write_ready), | |
| write_blocked: Arc::clone(&write_blocked), | |
| write_blocked_waker: Arc::clone(&write_blocked_waker), | |
| write_waker: Arc::clone(&write_waker), | |
| }, | |
| ControlledWebSocketHandle { | |
| inbound_tx, | |
| write_ready, | |
| write_blocked, | |
| write_blocked_waker, | |
| write_waker, | |
| }, | |
| outbound_rx, | |
| ) | |
| } | |
| } | |
| impl ControlledWebSocketHandle { | |
| fn send_inbound(&self, message: Message) -> anyhow::Result<()> { | |
| self.inbound_tx | |
| .unbounded_send(Ok(message)) | |
| .map_err(anyhow::Error::from) | |
| } | |
| fn set_write_ready(&self) { | |
| self.write_ready.store(true, Ordering::Release); | |
| self.write_waker.wake(); | |
| } | |
| async fn wait_for_blocked_write(&self) -> anyhow::Result<()> { | |
| timeout( | |
| Duration::from_secs(1), | |
| futures::future::poll_fn(|cx| { | |
| if self.write_blocked.load(Ordering::Acquire) { | |
| Poll::Ready(()) | |
| } else { | |
| self.write_blocked_waker.register(cx.waker()); | |
| Poll::Pending | |
| } | |
| }), | |
| ) | |
| .await?; | |
| Ok(()) | |
| } | |
| } | |
| impl Sink<Message> for ControlledWebSocket { | |
| type Error = std::convert::Infallible; | |
| fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { | |
| if self.write_ready.load(Ordering::Acquire) { | |
| Poll::Ready(Ok(())) | |
| } else { | |
| self.write_blocked.store(true, Ordering::Release); | |
| self.write_blocked_waker.wake(); | |
| self.write_waker.register(cx.waker()); | |
| Poll::Pending | |
| } | |
| } | |
| fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { | |
| self.outbound_tx | |
| .unbounded_send(item) | |
| .expect("test outbound receiver should stay open"); | |
| Ok(()) | |
| } | |
| fn poll_flush( | |
| self: Pin<&mut Self>, | |
| _cx: &mut Context<'_>, | |
| ) -> Poll<Result<(), Self::Error>> { | |
| Poll::Ready(Ok(())) | |
| } | |
| fn poll_close( | |
| self: Pin<&mut Self>, | |
| _cx: &mut Context<'_>, | |
| ) -> Poll<Result<(), Self::Error>> { | |
| Poll::Ready(Ok(())) | |
| } | |
| } | |
| impl Stream for ControlledWebSocket { | |
| type Item = Result<Message, std::convert::Infallible>; | |
| fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { | |
| Pin::new(&mut self.inbound_rx).poll_next(cx) | |
| } | |
| } | |
| } | |