/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/process/ipc.rs
764 строки
20 KB
nathanwhitbot
refactor: remove simd-json from process IPC parsing (#34477)
29 май 2026, 05:57
Не верифицирован
29 май 2026, 05:57
fa47334
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::io; use std::rc::Rc; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicUsize; use deno_core::AsyncRefCell; use deno_core::CancelHandle; use deno_core::ExternalOpsTracker; use deno_core::RcRef; use deno_core::serde_json; use deno_io::BiPipe; use deno_io::BiPipeRead; use deno_io::BiPipeWrite; use memchr::memchr; use tokio::io::AsyncWriteExt; #[cfg(unix)] type ReceivedRawFd = std::os::fd::OwnedFd; #[cfg(not(unix))] type ReceivedRawFd = i32; #[cfg(unix)] fn into_raw_fd_i32(fd: ReceivedRawFd) -> i32 { use std::os::fd::IntoRawFd; fd.into_raw_fd() } #[cfg(not(unix))] fn into_raw_fd_i32(fd: ReceivedRawFd) -> i32 { fd } /// Tracks whether the IPC resources is currently /// refed, and allows refing/unrefing it. pub struct IpcRefTracker { refed: AtomicBool, tracker: OpsTracker, } /// A little wrapper so we don't have to get an /// `ExternalOpsTracker` for tests. When we aren't /// cfg(test), this will get optimized out. enum OpsTracker { External(ExternalOpsTracker), #[cfg(test)] Test, } impl OpsTracker { fn ref_(&self) { match self { Self::External(tracker) => tracker.ref_op(), #[cfg(test)] Self::Test => {} } } fn unref(&self) { match self { Self::External(tracker) => tracker.unref_op(), #[cfg(test)] Self::Test => {} } } } impl IpcRefTracker { pub fn new(tracker: ExternalOpsTracker) -> Self { Self { refed: AtomicBool::new(false), tracker: OpsTracker::External(tracker), } } #[cfg(test)] fn new_test() -> Self { Self { refed: AtomicBool::new(false), tracker: OpsTracker::Test, } } pub fn ref_(&self) { if !self.refed.swap(true, std::sync::atomic::Ordering::AcqRel) { self.tracker.ref_(); } } pub fn unref(&self) { if self.refed.swap(false, std::sync::atomic::Ordering::AcqRel) { self.tracker.unref(); } } } async fn write_with_optional_fd( write_half: &mut BiPipeWrite, msg: &[u8], #[cfg_attr( not(unix), allow(unused_variables, reason = "fd transfer is unix-only") )] raw_fd: Option<i32>, ) -> Result<(), io::Error> { #[cfg(unix)] if let Some(raw_fd) = raw_fd { use std::os::fd::AsRawFd; use std::os::fd::FromRawFd; // `raw_fd` is an owned dup created for this IPC transfer. `sendmsg` attaches // it to the first bytes written, then this scope closes our local copy. // SAFETY: callers pass an owned fd returned by `fdForIpc`; this function // assumes ownership and closes it after the send attempt. let raw_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(raw_fd) }; let nwritten = write_half.send_with_fd(msg, raw_fd.as_raw_fd()).await?; if nwritten < msg.len() { // The fd has already been delivered with the first sendmsg. If the rest // of the frame fails, the reader must close the received fd while // discarding the malformed message. // TODO(nathanwhit): the receiver path (`IpcJsonStream::read_msg_inner`) // does not currently detect a truncated frame after an fd was attached. // If the writer half is dropped mid-message, the receiver will close // the IPC channel without explicitly closing the orphan fd. In practice // truncation here only happens on connection teardown, where the parent // process exit reaps everything anyway, so the leak window is bounded. write_half.write_all(&msg[nwritten..]).await?; } return Ok(()); } write_half.write_all(msg).await?; Ok(()) } pub struct IpcJsonStreamResource { pub read_half: AsyncRefCell<IpcJsonStream>, pub write_half: AsyncRefCell<BiPipeWrite>, pub cancel: Rc<CancelHandle>, pub queued_bytes: AtomicUsize, pub ref_tracker: IpcRefTracker, } impl deno_core::Resource for IpcJsonStreamResource { fn close(self: Rc<Self>) { self.cancel.cancel(); } } impl IpcJsonStreamResource { pub fn new( stream: i64, ref_tracker: IpcRefTracker, ) -> Result<Self, std::io::Error> { let (read_half, write_half) = BiPipe::from_raw(stream as _)?.split(); Ok(Self { read_half: AsyncRefCell::new(IpcJsonStream::new(read_half)), write_half: AsyncRefCell::new(write_half), cancel: Default::default(), queued_bytes: Default::default(), ref_tracker, }) } #[cfg(all(unix, test))] pub fn from_stream( stream: tokio::net::UnixStream, ref_tracker: IpcRefTracker, ) -> Self { let (read_half, write_half) = stream.into_split(); Self { read_half: AsyncRefCell::new(IpcJsonStream::new(read_half.into())), write_half: AsyncRefCell::new(write_half.into()), cancel: Default::default(), queued_bytes: Default::default(), ref_tracker, } } #[cfg(all(windows, test))] pub fn from_stream( pipe: tokio::net::windows::named_pipe::NamedPipeClient, ref_tracker: IpcRefTracker, ) -> Self { let (read_half, write_half) = tokio::io::split(pipe); Self { read_half: AsyncRefCell::new(IpcJsonStream::new(read_half.into())), write_half: AsyncRefCell::new(write_half.into()), cancel: Default::default(), queued_bytes: Default::default(), ref_tracker, } } /// Writes a newline-terminated JSON message to the IPC pipe. If `raw_fd` /// is `Some`, it must be an owned dup for this IPC transfer. It is sent /// alongside the first byte via SCM_RIGHTS on unix, then closed locally. pub async fn write_msg_bytes( self: Rc<Self>, msg: &[u8], raw_fd: Option<i32>, ) -> Result<(), io::Error> { let mut write_half = RcRef::map(self, |r| &r.write_half).borrow_mut().await; write_with_optional_fd(&mut write_half, msg, raw_fd).await } } // Initial capacity of the buffered reader and the JSON backing buffer. // // This is a tradeoff between memory usage and performance on large messages. // // 64kb has been chosen after benchmarking 64 to 66536 << 6 - 1 bytes per message. pub const INITIAL_CAPACITY: usize = 1024 * 64; /// A buffer for reading from the IPC pipe. /// Similar to the internal buffer of `tokio::io::BufReader`. /// /// This exists to provide buffered reading while avoiding a copy when a /// delimited message fits entirely in the read buffer. struct ReadBuffer { buffer: Box<[u8]>, pos: usize, cap: usize, #[cfg(unix)] raw_fd: Option<ReceivedRawFd>, } impl ReadBuffer { fn new() -> Self { Self { buffer: vec![0; INITIAL_CAPACITY].into_boxed_slice(), pos: 0, cap: 0, #[cfg(unix)] raw_fd: None, } } fn get_mut(&mut self) -> &mut [u8] { &mut self.buffer } fn available_mut(&mut self) -> &mut [u8] { &mut self.buffer[self.pos..self.cap] } fn consume(&mut self, n: usize) { self.pos = std::cmp::min(self.pos + n, self.cap); } fn needs_fill(&self) -> bool { self.pos >= self.cap } async fn fill_from_pipe( &mut self, pipe: &mut BiPipeRead, ) -> io::Result<usize> { #[cfg(unix)] { use std::os::fd::FromRawFd; let (nread, raw_fd) = pipe.recv_with_fd(self.get_mut()).await?; self.cap = nread; self.pos = 0; if let Some(raw_fd) = raw_fd { debug_assert!(self.raw_fd.is_none()); // SAFETY: `recv_with_fd` returns a newly received fd that this buffer // owns until the IPC reader takes it. let raw_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(raw_fd) }; if let Some(previous) = self.raw_fd.replace(raw_fd) { drop(previous); } } Ok(nread) } #[cfg(not(unix))] { use tokio::io::AsyncReadExt; let nread = pipe.read(self.get_mut()).await?; self.cap = nread; self.pos = 0; Ok(nread) } } #[cfg(unix)] fn take_raw_fd(&mut self) -> Option<ReceivedRawFd> { self.raw_fd.take() } #[cfg(not(unix))] fn take_raw_fd(&mut self) -> Option<i32> { None } } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum IpcJsonStreamError { #[class(inherit)] #[error("{0}")] Io(#[source] std::io::Error), #[class(generic)] #[error("{0}")] Json(#[source] serde_json::Error), } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum IpcAdvancedStreamError { #[class(inherit)] #[error("{0}")] Io(#[source] std::io::Error), } pub struct IpcAdvancedStream { pipe: BiPipeRead, read_buffer: ReadBuffer, } struct MessageLengthBuffer { buffer: [u8; 4], pos: u8, value: u32, } impl MessageLengthBuffer { fn new() -> Self { Self { buffer: [0; 4], pos: 0, value: 0, } } fn message_len(&mut self) -> Option<usize> { if self.pos == 4 { self.value = u32::from_be_bytes(self.buffer); self.pos = 5; Some(self.value as usize) } else if self.pos == 5 { Some(self.value as usize) } else { None } } fn update_pos_by(&mut self, num: usize) { self.pos = (self.pos + num as u8).min(4); } fn available_mut(&mut self) -> &mut [u8] { &mut self.buffer[self.pos as usize..4] } } impl IpcAdvancedStream { fn new(pipe: BiPipeRead) -> Self { Self { pipe, read_buffer: ReadBuffer::new(), } } pub async fn read_msg_bytes_and_raw_fd( &mut self, ) -> Result<Option<(Vec<u8>, Option<i32>)>, IpcAdvancedStreamError> { let mut length_buffer = MessageLengthBuffer::new(); let mut out_buf = Vec::with_capacity(32); let mut read = 0usize; let mut raw_fd = None; loop { if self.read_buffer.needs_fill() { self .read_buffer .fill_from_pipe(&mut self.pipe) .await .map_err(IpcAdvancedStreamError::Io)?; } if let Some(fd) = self.read_buffer.take_raw_fd() { debug_assert!(raw_fd.is_none()); if raw_fd.is_none() { raw_fd = Some(fd); } } let available = self.read_buffer.available_mut(); if available.is_empty() { if read == 0 { return Ok(None); } else if length_buffer.message_len().is_some() { return Err(IpcAdvancedStreamError::Io(io::Error::new( io::ErrorKind::UnexpectedEof, "ipc stream closed while reading message", ))); } else { return Err(IpcAdvancedStreamError::Io(io::Error::new( io::ErrorKind::UnexpectedEof, "ipc stream closed before message length", ))); } } let msg_len = length_buffer.message_len(); let (done, used) = if let Some(msg_len) = msg_len { if out_buf.len() >= msg_len { (true, 0) } else { let remaining = msg_len - out_buf.len(); out_buf.reserve(remaining); let to_copy = available.len().min(remaining); out_buf.extend_from_slice(&available[..to_copy]); (out_buf.len() == msg_len, to_copy) } } else { let len_avail = length_buffer.available_mut(); let to_copy = available.len().min(len_avail.len()); len_avail[..to_copy].copy_from_slice(&available[..to_copy]); length_buffer.update_pos_by(to_copy); (false, to_copy) }; self.read_buffer.consume(used); read += used; if done { return Ok(Some(( std::mem::take(&mut out_buf), raw_fd.map(into_raw_fd_i32), ))); } } } } pub struct IpcAdvancedStreamResource { pub read_half: AsyncRefCell<IpcAdvancedStream>, pub write_half: AsyncRefCell<BiPipeWrite>, pub cancel: Rc<CancelHandle>, pub queued_bytes: AtomicUsize, pub ref_tracker: IpcRefTracker, } impl IpcAdvancedStreamResource { pub fn new( stream: i64, ref_tracker: IpcRefTracker, ) -> Result<Self, std::io::Error> { let (read_half, write_half) = BiPipe::from_raw(stream as _)?.split(); Ok(Self { read_half: AsyncRefCell::new(IpcAdvancedStream::new(read_half)), write_half: AsyncRefCell::new(write_half), cancel: Default::default(), queued_bytes: Default::default(), ref_tracker, }) } /// Writes a serialized message to the IPC pipe. The first 4 bytes must be /// the length of the following message. If `raw_fd` is `Some`, see /// [`IpcJsonStreamResource::write_msg_bytes`] for the SCM_RIGHTS semantics. pub async fn write_msg_bytes( self: Rc<Self>, msg: &[u8], raw_fd: Option<i32>, ) -> Result<(), io::Error> { let mut write_half = RcRef::map(self, |r| &r.write_half).borrow_mut().await; write_with_optional_fd(&mut write_half, msg, raw_fd).await } } impl deno_core::Resource for IpcAdvancedStreamResource { fn close(self: Rc<Self>) { self.cancel.cancel(); } } // JSON serialization stream over IPC pipe. // // `\n` is used as a delimiter between messages. pub struct IpcJsonStream { pipe: BiPipeRead, buffer: Vec<u8>, read_buffer: ReadBuffer, } impl IpcJsonStream { fn new(pipe: BiPipeRead) -> Self { Self { pipe, buffer: Vec::with_capacity(INITIAL_CAPACITY), read_buffer: ReadBuffer::new(), } } pub async fn read_msg( &mut self, ) -> Result<Option<serde_json::Value>, IpcJsonStreamError> { Ok(self.read_msg_and_raw_fd().await?.map(|(msg, _)| msg)) } pub async fn read_msg_and_raw_fd( &mut self, ) -> Result<Option<(serde_json::Value, Option<i32>)>, IpcJsonStreamError> { let mut read = 0usize; let mut raw_fd = None; let mut json = None; loop { if self.read_buffer.needs_fill() { self .read_buffer .fill_from_pipe(&mut self.pipe) .await .map_err(IpcJsonStreamError::Io)?; } if let Some(fd) = self.read_buffer.take_raw_fd() { debug_assert!(raw_fd.is_none()); if raw_fd.is_none() { raw_fd = Some(fd); } } let available = self.read_buffer.available_mut(); if available.is_empty() { if read == 0 { return Ok(None); } else { return Err(IpcJsonStreamError::Io(io::Error::new( io::ErrorKind::UnexpectedEof, "ipc stream closed while reading message", ))); } } let (done, used) = if let Some(i) = memchr(b'\n', available) { if read == 0 { json.replace( serde_json::from_slice(&available[..i + 1]) .map_err(IpcJsonStreamError::Json)?, ); } else { self.buffer.extend_from_slice(&available[..=i]); } (true, i + 1) } else { self.buffer.extend_from_slice(available); (false, available.len()) }; self.read_buffer.consume(used); read += used; if done { let json = match json { Some(v) => v, None => serde_json::from_slice(&self.buffer[..read]) .map_err(IpcJsonStreamError::Json)?, }; // Safety: Same as `Vec::clear` but without the `drop_in_place` for // each element (nop for u8). Capacity remains the same. unsafe { self.buffer.set_len(0); } return Ok(Some((json, raw_fd.map(into_raw_fd_i32)))); } } } } #[allow(clippy::disallowed_methods, reason = "test code")] #[allow(clippy::print_stdout, reason = "test code")] #[cfg(test)] mod tests { use std::rc::Rc; use deno_core::RcRef; use deno_core::serde_json::json; use super::IpcJsonStreamResource; #[allow(clippy::unused_async, reason = "async on windows")] #[cfg(unix)] pub async fn pair() -> (Rc<IpcJsonStreamResource>, tokio::net::UnixStream) { let (a, b) = tokio::net::UnixStream::pair().unwrap(); /* Similar to how ops would use the resource */ let a = Rc::new(IpcJsonStreamResource::from_stream( a, super::IpcRefTracker::new_test(), )); (a, b) } #[cfg(windows)] pub async fn pair() -> ( Rc<IpcJsonStreamResource>, tokio::net::windows::named_pipe::NamedPipeServer, ) { use tokio::net::windows::named_pipe::ClientOptions; use tokio::net::windows::named_pipe::ServerOptions; let name = format!(r"\\.\pipe\deno-named-pipe-test-{}", rand::random::<u32>()); let server = ServerOptions::new().create(name.clone()).unwrap(); let client = ClientOptions::new().open(name).unwrap(); server.connect().await.unwrap(); /* Similar to how ops would use the resource */ let client = Rc::new(IpcJsonStreamResource::from_stream( client, super::IpcRefTracker::new_test(), )); (client, server) } #[tokio::test] async fn bench_ipc() -> Result<(), Box<dyn std::error::Error>> { // A simple round trip benchmark for quick dev feedback. // // Only ran when the env var is set. if std::env::var_os("BENCH_IPC_DENO").is_none() { return Ok(()); } let (ipc, mut fd2) = pair().await; let child = tokio::spawn(async move { use tokio::io::AsyncWriteExt; let size = 1024 * 1024; let stri = "x".repeat(size); let data = format!("\"{}\"\n", stri); for _ in 0..100 { fd2.write_all(data.as_bytes()).await?; } Ok::<_, std::io::Error>(()) }); let start = std::time::Instant::now(); let mut bytes = 0; let mut ipc = RcRef::map(ipc, |r| &r.read_half).borrow_mut().await; loop { let Some(msgs) = ipc.read_msg().await? else { break; }; bytes += msgs.as_str().unwrap().len(); if start.elapsed().as_secs() > 5 { break; } } let elapsed = start.elapsed(); let mb = bytes as f64 / 1024.0 / 1024.0; println!("{} mb/s", mb / elapsed.as_secs_f64()); child.await??; Ok(()) } #[tokio::test] async fn unix_ipc_json() -> Result<(), Box<dyn std::error::Error>> { let (ipc, mut fd2) = pair().await; let child = tokio::spawn(async move { use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; const EXPECTED: &[u8] = b"\"hello\"\n"; let mut buf = [0u8; EXPECTED.len()]; let n = fd2.read_exact(&mut buf).await?; assert_eq!(&buf[..n], EXPECTED); fd2.write_all(b"\"world\"\n").await?; Ok::<_, std::io::Error>(()) }); ipc .clone() .write_msg_bytes(&json_to_bytes(json!("hello")), None) .await?; let mut ipc = RcRef::map(ipc, |r| &r.read_half).borrow_mut().await; let msgs = ipc.read_msg().await?.unwrap(); assert_eq!(msgs, json!("world")); child.await??; Ok(()) } fn json_to_bytes(v: deno_core::serde_json::Value) -> Vec<u8> { let mut buf = deno_core::serde_json::to_vec(&v).unwrap(); buf.push(b'\n'); buf } #[tokio::test] async fn unix_ipc_json_multi() -> Result<(), Box<dyn std::error::Error>> { let (ipc, mut fd2) = pair().await; let child = tokio::spawn(async move { use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; const EXPECTED: &[u8] = b"\"hello\"\n\"world\"\n"; let mut buf = [0u8; EXPECTED.len()]; let n = fd2.read_exact(&mut buf).await?; assert_eq!(&buf[..n], EXPECTED); fd2.write_all(b"\"foo\"\n\"bar\"\n").await?; Ok::<_, std::io::Error>(()) }); ipc .clone() .write_msg_bytes(&json_to_bytes(json!("hello")), None) .await?; ipc .clone() .write_msg_bytes(&json_to_bytes(json!("world")), None) .await?; let mut ipc = RcRef::map(ipc, |r| &r.read_half).borrow_mut().await; let msgs = ipc.read_msg().await?.unwrap(); assert_eq!(msgs, json!("foo")); child.await??; Ok(()) } #[tokio::test] async fn unix_ipc_json_invalid() -> Result<(), Box<dyn std::error::Error>> { let (ipc, mut fd2) = pair().await; let child = tokio::spawn(async move { tokio::io::AsyncWriteExt::write_all(&mut fd2, b"\n\n").await?; Ok::<_, std::io::Error>(()) }); let mut ipc = RcRef::map(ipc, |r| &r.read_half).borrow_mut().await; let _err = ipc.read_msg().await.unwrap_err(); child.await??; Ok(()) } #[test] fn memchr() { let str = b"hello world"; assert_eq!(super::memchr(b'h', str), Some(0)); assert_eq!(super::memchr(b'w', str), Some(6)); assert_eq!(super::memchr(b'd', str), Some(10)); assert_eq!(super::memchr(b'x', str), None); let empty = b""; assert_eq!(super::memchr(b'\n', empty), None); } }