feat: SSH transport — clawsync sync user@host:path without pre-running server

Adds generic framed I/O transport (FramedReader/FramedWriter over any
AsyncRead/AsyncWrite) and a StreamPeer backed by boxed I/O, enabling
two new modes:

SSH client mode:
  clawsync sync <local> [user@]host:path
  Detects SSH syntax (@ or non-numeric port), spawns
  `ssh user@host clawsync serve-fs path --stdio`, and uses the child's
  stdin/stdout as the sync protocol channel. No pre-running server needed.

stdio server mode (--stdio flag on serve-fs / serve-all):
  clawsync serve-fs <dir> --stdio
  Reads/writes the sync protocol on stdin/stdout instead of binding TCP.
  Suppresses the startup message (stdout is the protocol channel).
  Used automatically by SSH clients; also usable in scripts/containers.

Transport layer (clawsync-transport):
  - New framed.rs: FramedReader<R>, FramedWriter<W>, StreamPeer, split halves
  - SyncPeer::Stream variant; into_pipe_halves() splits into StreamReadHalf/WriteHalf
  - PipeReadHalf::Stream, PipeWriteHalf::Stream for pipelined W=16 sessions

CLI (clawsync-cli):
  - ParsedRemote enum; parse_remote_target() handles both TCP and SSH syntax
  - connect_remote() dispatches to TCP/QUIC or ssh_connect()
  - cmd_sync and cmd_watch use connect_remote() — SSH works transparently
  - cmd_serve_fs and cmd_serve_all gain --stdio flag
  - 2 integration tests: SSH (skipped without CLAWSYNC_TEST_SSH=1), stdio no-panic

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
osobh
2026-04-05 13:24:44 -05:00
co-authored by Claude Sonnet 4.6
parent 75e2550d9e
commit 85b6edbdff
5 changed files with 449 additions and 45 deletions
+158 -43
View File
@@ -37,6 +37,7 @@ use std::sync::Arc;
use clawsync_fs::{FsSyncClient, FsSyncPullClient, FsSyncServer};
use clawsync_hdf5::{DatasetManifest, ReceivedDataset, apply_received_payloads};
use clawsync_transport::framed::StreamPeer;
use clawsync_transport::peer::{PipeWriteHalf, SyncPeer};
use clawsync_transport::protocol::SyncMessage;
use clawsync_transport::quic::{QuicConfig, QuicServer, quic_connect};
@@ -192,12 +193,16 @@ enum Commands {
///
/// Uses CDC-based delta transfer: only changed chunks are sent,
/// immune to insertion/deletion staircase unlike rsync.
///
/// Remote formats:
/// `host:port/path` — connect to a running `serve-fs` over TCP
/// `[user@]host:path` — connect via SSH (spawns `clawsync serve-fs --stdio`)
Sync {
/// Local file or directory path.
#[arg(value_name = "LOCAL_PATH")]
local: PathBuf,
/// Remote address and path: `host:port/remote/path`
#[arg(value_name = "HOST:PORT/REMOTE_PATH")]
/// Remote: `host:port/path` (TCP) or `[user@]host:path` (SSH)
#[arg(value_name = "REMOTE")]
remote: String,
/// Delete files on the remote that don't exist locally.
#[arg(long)]
@@ -233,6 +238,12 @@ enum Commands {
/// Use QUIC transport instead of TCP (self-signed TLS, for testing).
#[arg(long)]
quic: bool,
/// Read/write the sync protocol on stdin/stdout instead of a TCP socket.
///
/// Used automatically by SSH clients that spawn this process remotely.
/// In this mode no startup message is printed and --bind is ignored.
#[arg(long)]
stdio: bool,
},
/// Sync a local HDF5 file to a remote server at dataset granularity.
@@ -342,6 +353,9 @@ enum Commands {
/// Use QUIC transport instead of TCP (self-signed TLS, for testing).
#[arg(long)]
quic: bool,
/// Read/write the sync protocol on stdin/stdout (used by SSH clients).
#[arg(long)]
stdio: bool,
},
}
@@ -417,6 +431,105 @@ fn parse_remote(remote: &str) -> Result<(SocketAddr, PathBuf)> {
Ok((addr, PathBuf::from(path_str)))
}
// ─────────────────────────────────────────────────────────────────────────────
// SSH / TCP remote detection
// ─────────────────────────────────────────────────────────────────────────────
/// A parsed remote target — either a TCP socket address or an SSH endpoint.
enum ParsedRemote {
/// Direct TCP connection: `host:port/relative-path`
Tcp { addr: SocketAddr, _path: String },
/// SSH-tunnelled connection: `[user@]host:remote-path`
Ssh { user_host: String, path: String },
}
/// Parse a remote string into either a TCP or SSH target.
///
/// Heuristic:
/// * If the string contains `@` → SSH syntax (`[user@]host:path`)
/// * Otherwise try TCP (`host:port/path`): if there is no `/` or the port
/// segment is non-numeric → SSH syntax
fn parse_remote_target(remote: &str) -> Result<ParsedRemote> {
// Explicit SSH: [user@]host:path
if remote.contains('@') {
return Ok(parse_ssh_target(remote)?);
}
// Try TCP: host:port/path
if let Some(slash) = remote.find('/') {
let addr_str = &remote[..slash];
if let Ok(addr) = addr_str.parse::<SocketAddr>() {
return Ok(ParsedRemote::Tcp {
addr,
_path: remote[slash + 1..].to_string(),
});
}
}
// Fallback: treat as SSH (no @, but port is non-numeric or no slash)
parse_ssh_target(remote)
}
fn parse_ssh_target(remote: &str) -> Result<ParsedRemote> {
let colon = remote
.find(':')
.with_context(|| format!("SSH remote must be '[user@]host:path', got: {remote}"))?;
let user_host = remote[..colon].to_string();
let path = remote[colon + 1..].to_string();
if user_host.is_empty() {
anyhow::bail!("SSH remote must have a host before ':', got: {remote}");
}
Ok(ParsedRemote::Ssh { user_host, path })
}
/// Connect to a remote using either TCP or SSH depending on `target`.
async fn connect_remote(target: &ParsedRemote, quic: bool) -> Result<SyncPeer> {
match target {
ParsedRemote::Tcp { addr, .. } => {
if quic {
let cfg = QuicConfig::insecure()
.with_context(|| "failed to build QUIC insecure config")?;
Ok(SyncPeer::Quic(Arc::new(
quic_connect(*addr, "localhost", cfg)
.await
.with_context(|| format!("QUIC connect to {addr} failed"))?,
)))
} else {
Ok(SyncPeer::Tcp(
TcpConnection::connect(*addr)
.await
.with_context(|| format!("TCP connect to {addr} failed"))?,
))
}
}
ParsedRemote::Ssh { user_host, path } => {
if quic {
anyhow::bail!("--quic is not supported with SSH transport; use TCP or omit --quic");
}
ssh_connect(user_host, path).await
}
}
}
/// Spawn `ssh <user_host> clawsync serve-fs <path> --stdio` and wrap the
/// child's stdin/stdout as a `SyncPeer::Stream`.
async fn ssh_connect(user_host: &str, path: &str) -> Result<SyncPeer> {
use tokio::process::Command;
use std::process::Stdio;
let child = Command::new("ssh")
.arg(user_host)
.arg("clawsync")
.arg("serve-fs")
.arg(path)
.arg("--stdio")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("failed to spawn ssh {user_host}"))?;
Ok(SyncPeer::Stream(StreamPeer::from_child(child)))
}
// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────
@@ -1189,31 +1302,22 @@ async fn cmd_sync(
exclude: Vec<String>,
quic: bool,
) -> Result<()> {
let (addr, _remote_path) = parse_remote(&remote)?;
let target = parse_remote_target(&remote)?;
let excludes = build_glob_set(&exclude)?;
let transport_label = match (&target, quic) {
(ParsedRemote::Ssh { .. }, _) => "SSH",
(_, true) => "QUIC",
_ => "TCP",
};
println!(
"Syncing {}{} ({}) ...",
local.display(),
addr,
if quic { "QUIC" } else { "TCP" }
remote,
transport_label,
);
let peer: SyncPeer = if quic {
let cfg =
QuicConfig::insecure().with_context(|| "failed to build QUIC insecure config")?;
SyncPeer::Quic(Arc::new(
quic_connect(addr, "localhost", cfg)
.await
.with_context(|| format!("QUIC connect to {addr} failed"))?,
))
} else {
SyncPeer::Tcp(
TcpConnection::connect(addr)
.await
.with_context(|| format!("cannot connect to {addr}"))?,
)
};
let peer = connect_remote(&target, quic).await?;
let mut client = FsSyncClient::new(peer, local, excludes, delete);
if dry_run {
@@ -1298,20 +1402,25 @@ async fn cmd_watch(
use std::sync::mpsc as std_mpsc;
use std::time::{Duration, Instant};
let (addr, _remote_path) = parse_remote(&remote)?;
let target = parse_remote_target(&remote)?;
let excludes = build_glob_set(&exclude)?;
let debounce = Duration::from_millis(debounce_ms);
let transport_label = match (&target, quic) {
(ParsedRemote::Ssh { .. }, _) => "SSH",
(_, true) => "QUIC",
_ => "TCP",
};
println!(
"Watching {}{} ({}), debounce {}ms ...",
local.display(),
addr,
if quic { "QUIC" } else { "TCP" },
remote,
transport_label,
debounce_ms
);
// Run an initial sync before entering the watch loop.
watch_sync_once(&local, &excludes, delete, addr, quic).await?;
watch_sync_once(&local, &excludes, delete, &target, quic).await?;
// Set up OS file-system watcher.
let (tx, rx) = std_mpsc::channel::<notify::Result<notify::Event>>();
@@ -1348,7 +1457,7 @@ async fn cmd_watch(
}
}
// Re-sync, printing errors without exiting so the watcher stays alive.
if let Err(e) = watch_sync_once(&local, &excludes, delete, addr, quic).await {
if let Err(e) = watch_sync_once(&local, &excludes, delete, &target, quic).await {
eprintln!(" sync error: {e:#}");
}
}
@@ -1360,24 +1469,10 @@ async fn watch_sync_once(
local: &Path,
excludes: &globset::GlobSet,
delete: bool,
addr: std::net::SocketAddr,
target: &ParsedRemote,
quic: bool,
) -> Result<()> {
let peer: SyncPeer = if quic {
let cfg =
QuicConfig::insecure().with_context(|| "failed to build QUIC insecure config")?;
SyncPeer::Quic(Arc::new(
quic_connect(addr, "localhost", cfg)
.await
.with_context(|| format!("QUIC connect to {addr} failed"))?,
))
} else {
SyncPeer::Tcp(
TcpConnection::connect(addr)
.await
.with_context(|| format!("cannot connect to {addr}"))?,
)
};
let peer = connect_remote(target, quic).await?;
let stats = FsSyncClient::new(peer, local.to_path_buf(), excludes.clone(), delete)
.run()
.await?;
@@ -1448,9 +1543,19 @@ async fn cmd_serve_fs(
allow_delete: bool,
exclude: Vec<String>,
quic: bool,
stdio: bool,
) -> Result<()> {
let excludes = build_glob_set(&exclude)?;
// stdio mode: single session over stdin/stdout — used by SSH clients.
// No TCP binding, no startup message (stdout is the protocol channel).
if stdio {
let peer = SyncPeer::Stream(StreamPeer::from_stdio());
let srv = FsSyncServer::new(peer, dir, excludes, allow_delete);
srv.handle().await?;
return Ok(());
}
if quic {
let cfg =
QuicConfig::self_signed().with_context(|| "failed to build QUIC self-signed config")?;
@@ -1992,9 +2097,17 @@ async fn cmd_serve_all(
allow_delete: bool,
exclude: Vec<String>,
quic: bool,
stdio: bool,
) -> Result<()> {
let excludes = build_glob_set(&exclude)?;
if stdio {
let peer = SyncPeer::Stream(StreamPeer::from_stdio());
return handle_any_client(peer, dir, allow_delete, excludes)
.await
.map_err(Into::into);
}
if quic {
let cfg =
QuicConfig::self_signed().with_context(|| "failed to build QUIC self-signed config")?;
@@ -2133,7 +2246,8 @@ fn main() -> Result<()> {
allow_delete,
exclude,
quic,
} => cmd_serve_fs(dir, bind, allow_delete, exclude, quic).await,
stdio,
} => cmd_serve_fs(dir, bind, allow_delete, exclude, quic, stdio).await,
Commands::Hdf5Sync {
local,
remote,
@@ -2152,7 +2266,8 @@ fn main() -> Result<()> {
allow_delete,
exclude,
quic,
} => cmd_serve_all(dir, bind, allow_delete, exclude, quic).await,
stdio,
} => cmd_serve_all(dir, bind, allow_delete, exclude, quic, stdio).await,
}
})
}
+101
View File
@@ -989,3 +989,104 @@ fn fs_watch_new_file_picked_up() {
);
assert_files_equal(&src.path().join("new.bin"), &dst.path().join("new.bin"));
}
// ─────────────────────────────────────────────────────────────────────────────
// SSH / stdio transport tests
// ─────────────────────────────────────────────────────────────────────────────
/// Run `clawsync sync <src> <user@localhost:dst_path>` where the SSH target is
/// `localhost`. This spawns a real SSH connection, so it requires:
/// - SSH server running on localhost
/// - Password-less login (key-based auth) for the current user
/// - `clawsync` binary on the remote PATH (or the same binary path used here)
///
/// Guarded by the `CLAWSYNC_TEST_SSH` environment variable; skip when absent.
#[test]
fn fs_sync_ssh_transport_cold_copy() {
if std::env::var("CLAWSYNC_TEST_SSH").is_err() {
eprintln!("Skipping fs_sync_ssh_transport_cold_copy (set CLAWSYNC_TEST_SSH=1 to enable)");
return;
}
let src = TempDir::new().unwrap();
let dst = TempDir::new().unwrap();
fs::write(src.path().join("alpha.bin"), vec![0x11u8; 4096]).unwrap();
fs::write(src.path().join("beta.bin"), vec![0x22u8; 8192]).unwrap();
// SSH syntax: user@localhost:absolute_path
let user = std::env::var("USER").unwrap_or_else(|_| whoami());
let remote = format!("{user}@localhost:{}", dst.path().display());
let output = Command::new(BIN)
.arg("sync")
.arg(src.path())
.arg(&remote)
.output()
.expect("failed to spawn clawsync sync (SSH)");
assert!(
output.status.success(),
"SSH sync failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_dir_equal(src.path(), dst.path());
}
fn whoami() -> String {
std::process::Command::new("whoami")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string())
}
/// Directly exercise the `--stdio` protocol path without SSH:
/// spawn `clawsync serve-fs <dst> --stdio` with piped stdin/stdout, then run
/// `clawsync sync <src> localhost:9999/` against a real TCP server BUT
/// instead use the simpler approach: verify `serve-fs --stdio` exits cleanly
/// when the client closes the connection (smoke test).
///
/// This guards against regressions where `--stdio` panics or hangs on startup.
#[test]
fn fs_sync_stdio_mode_cold_copy() {
let src = TempDir::new().unwrap();
let dst = TempDir::new().unwrap();
fs::write(src.path().join("test.bin"), vec![0xABu8; 2048]).unwrap();
// Start serve-fs in stdio mode. Because `clawsync sync` spawns the server
// through SSH in real use, we replicate that here by starting the server
// as a child process with piped stdio AND running the client pointing at
// the server's address via a short-circuit: we use the SSH syntax which
// will call `ssh localhost clawsync serve-fs ... --stdio`.
//
// Instead, directly test that `clawsync serve-fs --stdio` starts cleanly
// and processes one session by using `clawsync sync` with SSH syntax to
// localhost (same as above). Since that requires SSH, just verify the
// subprocess doesn't panic on startup by sending a graceful close.
let mut child = Command::new(BIN)
.arg("serve-fs")
.arg(dst.path())
.arg("--stdio")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("failed to spawn serve-fs --stdio");
// Drop stdin immediately — the server should detect EOF and exit cleanly
// rather than panicking.
drop(child.stdin.take());
drop(child.stdout.take());
let status = child.wait().expect("serve-fs --stdio did not exit");
// Exit with an error is OK here (EOF on protocol = transport error),
// but it must NOT panic (exit code 101 on Rust panic).
assert_ne!(
status.code(),
Some(101),
"serve-fs --stdio must not panic on EOF"
);
}
+164
View File
@@ -0,0 +1,164 @@
//! Generic length-prefixed `SyncMessage` framing over any `AsyncRead`/`AsyncWrite`.
//!
//! The wire format is identical to [`crate::tcp`]:
//! ```text
//! [4 bytes LE length][body bytes]
//! ```
//!
//! Using trait objects (`Box<dyn AsyncRead + …>`) lets both child-process stdio
//! and terminal stdin/stdout share a single concrete type, which makes
//! `SyncPeer::Stream` work for both SSH-spawned servers and `--stdio` mode.
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::error::TransportError;
use crate::protocol::{MAX_FRAME_SIZE, SyncMessage};
// ─────────────────────────────────────────────────────────────────────────────
// FramedReader / FramedWriter
// ─────────────────────────────────────────────────────────────────────────────
/// The read half: deserialises length-prefixed `SyncMessage` frames.
pub struct FramedReader<R: AsyncRead + Unpin + Send> {
inner: R,
}
impl<R: AsyncRead + Unpin + Send> FramedReader<R> {
pub fn new(inner: R) -> Self {
Self { inner }
}
pub async fn recv(&mut self) -> Result<SyncMessage, TransportError> {
let mut len_buf = [0u8; 4];
self.inner.read_exact(&mut len_buf).await?;
let len = u32::from_le_bytes(len_buf) as usize;
if len > MAX_FRAME_SIZE {
return Err(TransportError::FrameTooLarge {
size: len,
max: MAX_FRAME_SIZE,
});
}
let mut body = vec![0u8; len];
self.inner.read_exact(&mut body).await?;
SyncMessage::from_bytes(&body).map_err(TransportError::Deserialization)
}
}
/// The write half: serialises `SyncMessage` frames with a 4-byte LE length prefix.
pub struct FramedWriter<W: AsyncWrite + Unpin + Send> {
inner: W,
}
impl<W: AsyncWrite + Unpin + Send> FramedWriter<W> {
pub fn new(inner: W) -> Self {
Self { inner }
}
pub async fn send(&mut self, msg: &SyncMessage) -> Result<(), TransportError> {
let body = msg.to_bytes().map_err(TransportError::Serialization)?;
let len = body.len() as u32;
self.inner.write_all(&len.to_le_bytes()).await?;
self.inner.write_all(&body).await?;
self.inner.flush().await?;
Ok(())
}
pub async fn shutdown(&mut self) -> Result<(), TransportError> {
self.inner.shutdown().await?;
Ok(())
}
}
// ─────────────────────────────────────────────────────────────────────────────
// StreamPeer — boxed I/O peer (child-process pipes or stdin/stdout)
// ─────────────────────────────────────────────────────────────────────────────
type BoxReader = Box<dyn AsyncRead + Unpin + Send>;
type BoxWriter = Box<dyn AsyncWrite + Unpin + Send>;
/// A `SyncMessage` peer backed by arbitrary boxed `AsyncRead` + `AsyncWrite`.
///
/// Used for:
/// * **SSH mode** — child-process `ChildStdout` / `ChildStdin`
/// * **`--stdio` mode** — `tokio::io::stdin()` / `tokio::io::stdout()`
pub struct StreamPeer {
reader: FramedReader<BoxReader>,
writer: FramedWriter<BoxWriter>,
/// Keeps a spawned child process alive for the lifetime of the peer.
_child: Option<tokio::process::Child>,
}
impl StreamPeer {
/// Wrap a spawned child process (takes ownership, keeps it alive).
pub fn from_child(mut child: tokio::process::Child) -> Self {
let stdout: BoxReader = Box::new(
child
.stdout
.take()
.expect("child must have piped stdout"),
);
let stdin: BoxWriter = Box::new(
child
.stdin
.take()
.expect("child must have piped stdin"),
);
Self {
reader: FramedReader::new(stdout),
writer: FramedWriter::new(stdin),
_child: Some(child),
}
}
/// Wrap this process's own stdin/stdout (for `--stdio` server mode).
pub fn from_stdio() -> Self {
Self {
reader: FramedReader::new(Box::new(tokio::io::stdin())),
writer: FramedWriter::new(Box::new(tokio::io::stdout())),
_child: None,
}
}
pub async fn send(&mut self, msg: &SyncMessage) -> Result<(), TransportError> {
self.writer.send(msg).await
}
pub async fn recv(&mut self) -> Result<SyncMessage, TransportError> {
self.reader.recv().await
}
pub async fn shutdown(&mut self) -> Result<(), TransportError> {
self.writer.shutdown().await
}
/// Split into independent halves for pipelined I/O.
pub fn into_split(self) -> (StreamReadHalf, StreamWriteHalf) {
(StreamReadHalf(self.reader), StreamWriteHalf(self.writer))
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Split halves
// ─────────────────────────────────────────────────────────────────────────────
pub struct StreamReadHalf(pub(crate) FramedReader<BoxReader>);
impl StreamReadHalf {
pub async fn recv(&mut self) -> Result<SyncMessage, TransportError> {
self.0.recv().await
}
}
pub struct StreamWriteHalf(pub(crate) FramedWriter<BoxWriter>);
impl StreamWriteHalf {
pub async fn send(&mut self, msg: &SyncMessage) -> Result<(), TransportError> {
self.0.send(msg).await
}
pub async fn shutdown(&mut self) -> Result<(), TransportError> {
self.0.shutdown().await
}
}
+4 -1
View File
@@ -1,14 +1,16 @@
//! ClawSync transport backends: TCP, QUIC, and mmap.
//! ClawSync transport backends: TCP, QUIC, mmap, and stream (stdio / process).
//!
//! - [`protocol`]: `SyncMessage` wire protocol + length-prefixed framing
//! - [`tcp`]: Async TCP transport (tokio)
//! - [`quic`]: QUIC transport (quinn 0.11, TLS 1.3)
//! - [`mmap`]: Memory-mapped local transport — zero-copy same-node IPC
//! - [`framed`]: Generic length-prefixed framing over any `AsyncRead`/`AsyncWrite`
//! - [`error`] — [`TransportError`]
#![deny(unsafe_code)]
pub mod error;
pub mod framed;
pub mod mmap;
pub mod peer;
pub mod protocol;
@@ -16,6 +18,7 @@ pub mod quic;
pub mod tcp;
pub use error::TransportError;
pub use framed::StreamPeer;
pub use mmap::{DEFAULT_CAPACITY, MmapChannel, MmapReceiver, MmapSender};
pub use peer::{PipeReadHalf, PipeWriteHalf, SyncPeer};
pub use protocol::SyncMessage;
+22 -1
View File
@@ -15,6 +15,7 @@
use std::sync::Arc;
use crate::error::TransportError;
use crate::framed::{StreamPeer, StreamReadHalf, StreamWriteHalf};
use crate::mmap::{MmapReceiver, MmapSender};
use crate::protocol::SyncMessage;
use crate::quic::QuicConnection;
@@ -24,7 +25,7 @@ use crate::tcp::{TcpConnection, TcpReadHalf, TcpWriteHalf};
// SyncPeer
// ─────────────────────────────────────────────────────────────────────────────
/// A unified connection handle for a single sync session (TCP, QUIC, or mmap).
/// A unified connection handle for a single sync session (TCP, QUIC, mmap, or stream).
pub enum SyncPeer {
Tcp(TcpConnection),
Quic(Arc<QuicConnection>),
@@ -35,6 +36,9 @@ pub enum SyncPeer {
send_ch: MmapSender,
recv_ch: MmapReceiver,
},
/// Generic boxed-I/O peer — covers SSH child-process pipes and `--stdio`
/// mode (server reads/writes on its own stdin/stdout).
Stream(StreamPeer),
}
impl SyncPeer {
@@ -45,6 +49,7 @@ impl SyncPeer {
SyncPeer::Mmap { send_ch, .. } => {
tokio::task::block_in_place(|| send_ch.send(msg))
}
SyncPeer::Stream(s) => s.send(msg).await,
}
}
@@ -55,6 +60,7 @@ impl SyncPeer {
SyncPeer::Mmap { recv_ch, .. } => {
tokio::task::block_in_place(|| recv_ch.recv())
}
SyncPeer::Stream(s) => s.recv().await,
}
}
@@ -66,9 +72,15 @@ impl SyncPeer {
Ok(())
}
SyncPeer::Mmap { .. } => Ok(()),
SyncPeer::Stream(mut s) => s.shutdown().await,
}
}
/// Return the inner `StreamPeer` if this is a `Stream` variant, otherwise `None`.
pub fn as_stream_peer(&self) -> Option<&StreamPeer> {
if let SyncPeer::Stream(s) = self { Some(s) } else { None }
}
/// Return a cloned `Arc` if this is a QUIC connection, otherwise `None`.
///
/// This is used by servers to keep the QUIC connection alive after the
@@ -97,6 +109,10 @@ impl SyncPeer {
SyncPeer::Mmap { send_ch, recv_ch } => {
(PipeReadHalf::Mmap(recv_ch), PipeWriteHalf::Mmap(send_ch))
}
SyncPeer::Stream(s) => {
let (r, w) = s.into_split();
(PipeReadHalf::Stream(r), PipeWriteHalf::Stream(w))
}
}
}
}
@@ -110,6 +126,7 @@ pub enum PipeWriteHalf {
Tcp(TcpWriteHalf),
Quic(Arc<QuicConnection>),
Mmap(MmapSender),
Stream(StreamWriteHalf),
}
impl PipeWriteHalf {
@@ -118,6 +135,7 @@ impl PipeWriteHalf {
PipeWriteHalf::Tcp(h) => h.send(msg).await,
PipeWriteHalf::Quic(c) => c.send(msg).await,
PipeWriteHalf::Mmap(tx) => tokio::task::block_in_place(|| tx.send(msg)),
PipeWriteHalf::Stream(w) => w.send(msg).await,
}
}
@@ -129,6 +147,7 @@ impl PipeWriteHalf {
Ok(())
}
PipeWriteHalf::Mmap(_) => Ok(()),
PipeWriteHalf::Stream(mut w) => w.shutdown().await,
}
}
}
@@ -138,6 +157,7 @@ pub enum PipeReadHalf {
Tcp(TcpReadHalf),
Quic(Arc<QuicConnection>),
Mmap(MmapReceiver),
Stream(StreamReadHalf),
}
impl PipeReadHalf {
@@ -146,6 +166,7 @@ impl PipeReadHalf {
PipeReadHalf::Tcp(h) => h.recv().await,
PipeReadHalf::Quic(c) => c.recv().await,
PipeReadHalf::Mmap(rx) => tokio::task::block_in_place(|| rx.recv()),
PipeReadHalf::Stream(r) => r.recv().await,
}
}
}