fix: UdpHalf is the FRAMED adapter (U-1) — U-2 bind posture, U-3 lazy scratch
Review 001 remediation (tasks/tunnels/fix-udp-framed-adapter): - U-1 [major]: UdpHalf now composes the ADR-003 codec at the substrate boundary — poll_write de-frames channel bytes (incremental DatagramReader) and sends each payload as one raw datagram; poll_read frames raw target datagrams and hands frame bytes to the pump. Targets see payload-only bytes; empty datagrams round-trip (the F-2 fix end-to-end); >8 KiB datagrams arrive as ONE datagram. Mechanics the verification gates forced beyond the sketch: poll_write accepts all offered bytes (tokio copy re-offers a full window unsatisfied — partial-frame holds deadlock >8 KiB frames), backpressure is an accepted-byte high-water mark, socket drain pumps on write/flush/shutdown; >caller-buffer frames stage across reads (the truncation probe's assertion surface updated per the re-validation correction). - U-2 [minor]: connect_udp resolves the target and binds the unspecified address of the matching family (v6 targets dial from [::]:0); bind_tcp gains the wildcard-exposure doc warning. - U-3 [minor]: the 65535 recv scratch allocates lazily on first recv (full-size read kept — try_recv truncates silently; the MTU-shrink idea confirmed unsafe in re-validation). - N-1: the module/connect_udp/UdpHalf docs now describe the composed codec truthfully. Gates (permanent tests in tests/local_halves.rs): recording-target suite (payload-only at the target, empty-datagram round-trip, 9000- byte single datagram, unframed-target resolution), the 65507-byte max-datagram staging probe, and the v6 dial gate. Docs: ADR-003 §Amendment (the boundary mechanics), wire.md truncation bullet, consumer.md adopt sketch (substrate param). Review 001 resolution notes on U-1/U-2/U-3/N-1; task marked completed. Verified: 63 default / 76 local tests ×3 repeat runs; clippy -D warnings (all-targets + wasm32), fmt --check, doc --no-deps (warning-free), wasm check, publish dry-run.
This commit is contained in:
1 parent
aa7b5b7169
commit
8cf4cc8bb5
7 files changed
+765
-106
No files matched your search
+293
-67
@@ -10,8 +10,9 @@
|
||||
//! [`TcpListenerHalves`] accept half for listen producers.
|
||||
//! - **UDP** — [`connect_udp`]: a connected `UdpSocket` wrapped in the
|
||||
//! FRAMED adapter ([`UdpHalf`], ADR-003 — the codec composes at this
|
||||
//! boundary; the pump never sees UDP specifics). Truncation fails
|
||||
//! loud (OQ-TN-13's resolved posture).
|
||||
//! boundary: the channel side speaks the `[len: u16 BE]` frames, the
|
||||
//! target side sees payload-only datagrams; the pump never sees UDP
|
||||
//! specifics). Truncation fails loud (OQ-TN-13's resolved posture).
|
||||
//! - **Unix** — [`dial_unix`], in v1 per OQ-TN-14 (same halves shape
|
||||
//! as TCP). **Stdio is NOT here** — a spawned process's stdio is
|
||||
//! alktty's pipe mode (exit codes + signals are tty semantics);
|
||||
@@ -29,6 +30,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::params::Substrate;
|
||||
use crate::producer::{AcceptQueue, DialFn, TargetHandle, TunnelEstablishError};
|
||||
use crate::wire::{frame_datagram, DatagramCodecError, DatagramReader, MAX_DATAGRAM_LEN};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
/// The dial/connect timeout (the POCs' 5s posture).
|
||||
@@ -54,6 +56,12 @@ pub async fn dial_tcp(target: &str) -> Result<TargetHandle, TunnelEstablishError
|
||||
/// Bind a TCP listener (the listen shape's assembly-side half).
|
||||
/// The binding decision belongs to the caller (OQ-TN-04) — this is a
|
||||
/// `local`-feature convenience, not protocol behavior.
|
||||
///
|
||||
/// **Bind posture (review 001 U-2):** the caller-supplied address is
|
||||
/// deliberate — a wildcard bind (`0.0.0.0` or `::`; note `::1` is
|
||||
/// loopback, not a wildcard) is the caller's explicit choice and
|
||||
/// exposes the listener per the host's firewall. There is no implicit
|
||||
/// wildcard default here.
|
||||
pub async fn bind_tcp(addr: &str) -> Result<TcpListenerHalves, TunnelEstablishError> {
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
@@ -106,14 +114,36 @@ impl TcpListenerHalves {
|
||||
|
||||
// --- UDP ----------------------------------------------------------------
|
||||
|
||||
/// Connect a UDP "tunnel" to a target: bind an ephemeral socket,
|
||||
/// `connect(target)` (the associated-socket posture — one peer), and
|
||||
/// wrap it in the FRAMED adapter (ADR-003): `UdpHalf` enforces one
|
||||
/// datagram per read/write at the substrate boundary; the `[len: u16
|
||||
/// BE]` codec composes at this boundary — the pump never sees UDP
|
||||
/// Connect a UDP "tunnel" to a target: resolve the target, bind an
|
||||
/// ephemeral socket on the MATCHING address family (never an implicit
|
||||
/// wildcard mismatch — a v6-only target dials from a v6-bound socket;
|
||||
/// review 001 U-2's posture), `connect(target)` (the
|
||||
/// associated-socket posture — one peer), and wrap it in the FRAMED
|
||||
/// adapter (ADR-003): `UdpHalf` composes the `[len: u16 BE]` codec at
|
||||
/// this boundary — the channel side speaks framed bytes, the target
|
||||
/// side sees payload-only datagrams; the pump never sees UDP
|
||||
/// specifics.
|
||||
pub async fn connect_udp(target: &str) -> Result<TargetHandle, TunnelEstablishError> {
|
||||
let sock = tokio::net::UdpSocket::bind(("0.0.0.0", 0))
|
||||
use std::net::ToSocketAddrs;
|
||||
// Same posture as tokio's own `UdpSocket::connect(target: &str)` —
|
||||
// it resolves inline too; numeric targets (the registry's shape)
|
||||
// make this a non-blocking parse.
|
||||
let resolved = target
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| TunnelEstablishError::DialFailed(format!("udp resolve `{target}`: {e}")))?
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
TunnelEstablishError::DialFailed(format!("udp resolve `{target}`: no addresses"))
|
||||
})?;
|
||||
let bind_addr = match resolved {
|
||||
std::net::SocketAddr::V4(_) => {
|
||||
std::net::SocketAddr::new(std::net::Ipv4Addr::UNSPECIFIED.into(), 0)
|
||||
}
|
||||
std::net::SocketAddr::V6(_) => {
|
||||
std::net::SocketAddr::new(std::net::Ipv6Addr::UNSPECIFIED.into(), 0)
|
||||
}
|
||||
};
|
||||
let sock = tokio::net::UdpSocket::bind(bind_addr)
|
||||
.await
|
||||
.map_err(|e| TunnelEstablishError::ResourceShortage(format!("udp bind failed: {e}")))?;
|
||||
sock.connect(target)
|
||||
@@ -123,24 +153,55 @@ pub async fn connect_udp(target: &str) -> Result<TargetHandle, TunnelEstablishEr
|
||||
Ok(TargetHandle { read, write })
|
||||
}
|
||||
|
||||
/// A UDP socket presented as `AsyncRead`/`AsyncWrite` halves: reads
|
||||
/// yield whole datagrams (one `poll_recv` per read); writes send one
|
||||
/// datagram per `poll_write` buffer. The FRAMED adapter — the codec
|
||||
/// wraps at this boundary (ADR-003); the pump never sees UDP
|
||||
/// specifics.
|
||||
/// The FRAMED UDP adapter (ADR-003's substrate-boundary codec): the
|
||||
/// pump-facing face speaks the tunnel's `[len: u16 BE][payload]`
|
||||
/// channel framing; the socket face speaks whole raw datagrams.
|
||||
/// Orientation (review 001 §Re-validation — the fix sketch's caution):
|
||||
/// `UdpHalf`'s caller is the pump, and the CHANNEL side is the framed
|
||||
/// side —
|
||||
///
|
||||
/// **Truncation fails loud** (OQ-TN-13's resolved posture, ADR-003):
|
||||
/// `poll_recv` into a too-small caller buffer returns
|
||||
/// `io::ErrorKind::InvalidData` (the datagram is dropped with the
|
||||
/// error — silent truncation would corrupt the framing invariants).
|
||||
/// The codec's u16 bound keeps well-formed peers from ever hitting
|
||||
/// this path; only a mis-framed wire does.
|
||||
/// - `poll_write` is fed CHANNEL bytes (already-framed frames, from
|
||||
/// the session's codec) → it **de-frames** (an incremental
|
||||
/// [`DatagramReader`]) and sends each decoded payload to the target
|
||||
/// as one raw datagram. A frame may straddle successive poll_write
|
||||
/// buffers (the pump's 8 KiB copy chunks split large frames); the
|
||||
/// decoder carries the partial state until the frame completes.
|
||||
/// `len=0` decodes to a zero-payload datagram — a real empty
|
||||
/// datagram, NOT EOF (the F-2 fix end-to-end).
|
||||
/// - `poll_read` yields bytes destined for the CHANNEL (the session's
|
||||
/// codec decodes there) → it **frames** ([`frame_datagram`]) each
|
||||
/// raw target datagram and hands the frame bytes to the pump. A
|
||||
/// frame larger than the caller's buffer is staged and handed out
|
||||
/// across successive reads — a partial frame is never misread as a
|
||||
/// shorter one.
|
||||
///
|
||||
/// The target sees payload-only bytes; an empty datagram crosses the
|
||||
/// pump as its 2-byte frame (unambiguous with EOF).
|
||||
///
|
||||
/// **Truncation never happens silently** (OQ-TN-13's fail-loud
|
||||
/// posture, ADR-003): the socket-facing recv uses a full 65535-byte
|
||||
/// scratch (`try_recv` truncates at the buffer size — a smaller
|
||||
/// scratch would corrupt the framing before any size check runs), and
|
||||
/// a datagram beyond [`MAX_DATAGRAM_LEN`] (impossible for a
|
||||
/// well-formed peer — the u16 frame bound) errors loudly instead of
|
||||
/// truncating.
|
||||
///
|
||||
/// The scratch is allocated lazily on the first recv (review 001 U-3:
|
||||
/// a 64 KiB eager allocation per tunnel is needless for the write
|
||||
/// half and for tunnels that never recv).
|
||||
pub struct UdpHalf {
|
||||
sock: Arc<tokio::net::UdpSocket>,
|
||||
kind: UdpHalfKind,
|
||||
/// The recv scratch buffer (the fail-loud truncation check reads
|
||||
/// whole datagrams before copying into the caller's buffer).
|
||||
recv_scratch: Vec<u8>,
|
||||
/// Read-half state: the recv scratch (lazily grown to the full
|
||||
/// 65535 on first use — `try_recv` truncates silently at the
|
||||
/// buffer size, so the full-size read is correctness-load-bearing)
|
||||
/// plus the frame-staging state for handing a >caller-buffer
|
||||
/// frame out across reads.
|
||||
stage: UdpReadStage,
|
||||
/// Write-half state: the incremental de-framer fed by successive
|
||||
/// poll_write buffers, the decoded datagrams queued for the
|
||||
/// socket, and the accepted-byte backpressure accounting.
|
||||
deframe: UdpWriteDeFrame,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
@@ -149,6 +210,40 @@ enum UdpHalfKind {
|
||||
Write,
|
||||
}
|
||||
|
||||
/// Read-half staging: framed bytes awaiting the pump, chunked to the
|
||||
/// caller's buffer.
|
||||
struct UdpReadStage {
|
||||
/// The frame bytes not yet handed out (a full frame when `None`
|
||||
/// staged-partial state exists; empty between datagrams).
|
||||
pending: bytes::Bytes,
|
||||
/// Lazily allocated recv scratch (full 65535 on first recv).
|
||||
recv_scratch: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Write-half staging: the incremental de-framer, the decoded
|
||||
/// datagrams queued for the socket, and the datagram mid-send (blocked
|
||||
/// on socket send readiness).
|
||||
struct UdpWriteDeFrame {
|
||||
reader: DatagramReader,
|
||||
/// Decoded datagrams awaiting send (FIFO). poll_write accepts all
|
||||
/// offered bytes into the de-framer — the pump's copy window
|
||||
/// cannot be held closed for later top-up (a full window is
|
||||
/// re-offered unsatisfied until drained); backpressure is the
|
||||
/// accepted-byte high-water mark instead.
|
||||
queue: std::collections::VecDeque<bytes::Bytes>,
|
||||
/// The datagram mid-send (the socket returned WouldBlock under
|
||||
/// it — retried first on every poll).
|
||||
sending: Option<bytes::Bytes>,
|
||||
/// Accepted-byte accounting for the high-water mark (the bytes
|
||||
/// accepted but not yet sent on the socket).
|
||||
accepted: usize,
|
||||
}
|
||||
|
||||
/// The accepted-byte bound before poll_write applies backpressure
|
||||
/// (Pending). The pump's copy window is 8 KiB; this bounds the queue
|
||||
/// at ~16 full-size frames' worth of accepted-but-unsent bytes.
|
||||
const WRITE_QUEUE_HIGH_WATER: usize = 128 * 1024;
|
||||
|
||||
impl UdpHalf {
|
||||
pub fn split(
|
||||
sock: tokio::net::UdpSocket,
|
||||
@@ -157,18 +252,35 @@ impl UdpHalf {
|
||||
Box<dyn AsyncWrite + Send + Sync + Unpin>,
|
||||
) {
|
||||
let sock = Arc::new(sock);
|
||||
(
|
||||
Box::new(UdpHalf {
|
||||
sock: Arc::clone(&sock),
|
||||
kind: UdpHalfKind::Read,
|
||||
recv_scratch: vec![0u8; 65535],
|
||||
}),
|
||||
Box::new(UdpHalf {
|
||||
sock,
|
||||
kind: UdpHalfKind::Write,
|
||||
recv_scratch: Vec::new(),
|
||||
}),
|
||||
)
|
||||
let read_half = UdpHalf {
|
||||
sock: Arc::clone(&sock),
|
||||
kind: UdpHalfKind::Read,
|
||||
stage: UdpReadStage {
|
||||
pending: bytes::Bytes::new(),
|
||||
recv_scratch: None,
|
||||
},
|
||||
deframe: UdpWriteDeFrame {
|
||||
reader: DatagramReader::new(),
|
||||
queue: std::collections::VecDeque::new(),
|
||||
sending: None,
|
||||
accepted: 0,
|
||||
},
|
||||
};
|
||||
let write_half = UdpHalf {
|
||||
sock,
|
||||
kind: UdpHalfKind::Write,
|
||||
stage: UdpReadStage {
|
||||
pending: bytes::Bytes::new(),
|
||||
recv_scratch: None,
|
||||
},
|
||||
deframe: UdpWriteDeFrame {
|
||||
reader: DatagramReader::new(),
|
||||
queue: std::collections::VecDeque::new(),
|
||||
sending: None,
|
||||
accepted: 0,
|
||||
},
|
||||
};
|
||||
(Box::new(read_half), Box::new(write_half))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,40 +290,71 @@ impl AsyncRead for UdpHalf {
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
use std::task::Poll;
|
||||
let this = self.get_mut();
|
||||
debug_assert_eq!(this.kind, UdpHalfKind::Read);
|
||||
// One recv yields exactly one datagram into the caller's
|
||||
// buffer. The fail-loud posture (OQ-TN-13, ADR-003): a
|
||||
// datagram LARGER than the caller's buffer is a framing
|
||||
// violation — reported as an error, never silently truncated
|
||||
// (`poll_recv`-into-ReadBuf would truncate; the scratch-buffer
|
||||
// recv + size check keeps the invariant observable). The
|
||||
// codec's u16 bound keeps well-formed peers off this path.
|
||||
let scratch = &mut this.recv_scratch;
|
||||
|
||||
// Hand out staged frame bytes first (a frame larger than the
|
||||
// caller's buffer was split across reads; the frame's length
|
||||
// prefix already crossed, so the pump's de-framer on the far
|
||||
// side reassembles — no shorter-datagram misread is possible).
|
||||
if !this.stage.pending.is_empty() {
|
||||
let n = this.stage.pending.len().min(buf.remaining());
|
||||
buf.put_slice(&this.stage.pending.split_to(n));
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Recv one raw datagram (full-size scratch — try_recv
|
||||
// truncates silently at the buffer size, so the scratch must
|
||||
// hold the largest possible datagram before any size check).
|
||||
if this.stage.recv_scratch.is_none() {
|
||||
this.stage.recv_scratch = Some(vec![0u8; MAX_DATAGRAM_LEN]);
|
||||
}
|
||||
let scratch = this
|
||||
.stage
|
||||
.recv_scratch
|
||||
.as_deref_mut()
|
||||
.expect("scratch allocated above");
|
||||
match this.sock.try_recv(scratch) {
|
||||
Ok(n) => {
|
||||
if n > buf.remaining() {
|
||||
return std::task::Poll::Ready(Err(std::io::Error::new(
|
||||
let framed = frame_datagram(&scratch[..n]).map_err(|e| match e {
|
||||
// A >u16 datagram cannot exist on a well-formed
|
||||
// wire (the IP theoretical max is 65507); treat it
|
||||
// as the fail-loud framing violation it is.
|
||||
DatagramCodecError::Oversize(len, max) => std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"udp datagram {n} bytes exceeds caller buffer {} (truncation fails loud — OQ-TN-13)",
|
||||
buf.remaining()
|
||||
"udp datagram {len} bytes exceeds the codec frame bound {max} \
|
||||
(truncation fails loud — OQ-TN-13)"
|
||||
),
|
||||
)));
|
||||
),
|
||||
DatagramCodecError::InvalidLength { .. } => {
|
||||
unreachable!("frame_datagram never yields InvalidLength")
|
||||
}
|
||||
})?;
|
||||
if framed.len() <= buf.remaining() {
|
||||
buf.put_slice(&framed);
|
||||
} else {
|
||||
// The frame exceeds the caller's buffer: hand out
|
||||
// the first slice, stage the rest (the length
|
||||
// prefix rode along — the far-side de-framer
|
||||
// reassembles across reads).
|
||||
let n = buf.remaining();
|
||||
buf.put_slice(&framed[..n]);
|
||||
this.stage.pending = framed.slice(n..);
|
||||
}
|
||||
buf.put_slice(&scratch[..n]);
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
// Not ready: poll readiness to register the waker,
|
||||
// then Pending (the next wake retries the recv).
|
||||
match this.sock.poll_recv_ready(cx) {
|
||||
std::task::Poll::Ready(Ok(())) => std::task::Poll::Pending,
|
||||
std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Err(e)),
|
||||
std::task::Poll::Pending => std::task::Poll::Pending,
|
||||
Poll::Ready(Ok(())) => Poll::Pending,
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
Err(e) => std::task::Poll::Ready(Err(e)),
|
||||
Err(e) => Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,39 +365,122 @@ impl tokio::io::AsyncWrite for UdpHalf {
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
use std::task::Poll;
|
||||
let this = self.get_mut();
|
||||
debug_assert_eq!(this.kind, UdpHalfKind::Write);
|
||||
match this.sock.try_send(buf) {
|
||||
Ok(n) => std::task::Poll::Ready(Ok(n)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
// Poll send-readiness to register the waker, then
|
||||
// Pending (the next wake retries the send).
|
||||
match this.sock.poll_send_ready(cx) {
|
||||
std::task::Poll::Ready(Ok(())) => std::task::Poll::Pending,
|
||||
std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Err(e)),
|
||||
std::task::Poll::Pending => std::task::Poll::Pending,
|
||||
let offered = buf.len();
|
||||
|
||||
// Backpressure: if too many accepted bytes are still queued
|
||||
// for the socket, try to drain first; if the queue is still
|
||||
// over the line, return Pending (the pump's copy re-offers
|
||||
// its window unsatisfied — safe, nothing is lost).
|
||||
if this.deframe.accepted > WRITE_QUEUE_HIGH_WATER {
|
||||
match this.pump_queue_to_socket(cx) {
|
||||
Poll::Ready(Ok(_)) => {}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => {
|
||||
if this.deframe.accepted > WRITE_QUEUE_HIGH_WATER {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => std::task::Poll::Ready(Err(e)),
|
||||
}
|
||||
|
||||
// Accept ALL offered bytes into the incremental de-framer and
|
||||
// report them all consumed. The pump's copy window cannot be
|
||||
// held closed for a later top-up (a full window is re-offered
|
||||
// unsatisfied until drained — a partial-frame tail could sit
|
||||
// forever in a full window), and reporting fewer than all
|
||||
// bytes risks the copy's `WriteZero` on an empty re-offer.
|
||||
// The DatagramReader's partial-frame state is bounded by
|
||||
// construction (the u16 cap — review 001 N-2), so accepting
|
||||
// everything is bounded memory; socket-side backpressure is
|
||||
// the accepted-byte high-water mark above.
|
||||
let dgs = match this.deframe.reader.feed(buf) {
|
||||
Ok(dgs) => dgs,
|
||||
Err(e) => {
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
|
||||
}
|
||||
};
|
||||
for dg in dgs {
|
||||
this.deframe.accepted += dg.len() + 2;
|
||||
this.deframe.queue.push_back(dg);
|
||||
}
|
||||
|
||||
// Opportunistic send: drain whatever the socket accepts now.
|
||||
// The waker is registered on WouldBlock (the next
|
||||
// poll_send_ready wake retries); the queue never blocks
|
||||
// poll_write's accept path unless the high-water mark is hit.
|
||||
let _ = this.pump_queue_to_socket(cx);
|
||||
|
||||
Poll::Ready(Ok(offered))
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
// The send pump: tokio's copy polls flush after draining its
|
||||
// window, so the queued datagrams drain here whenever the
|
||||
// pump cycles — and a flush-resolved Ready(Ok(())) tells the
|
||||
// copy the bytes are on the wire. UDP sends are all-or-nothing
|
||||
// per datagram; a WouldBlock keeps the datagram queued (the
|
||||
// send waker is registered) and the queue persists until it
|
||||
// drains.
|
||||
self.get_mut().pump_queue_to_socket(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
// UDP has no shutdown; drop is the close.
|
||||
// UDP has no shutdown; drop is the close. Drain the queue
|
||||
// best-effort on the way out (readiness is already there if
|
||||
// the datagrams were sendable).
|
||||
let _ = self.get_mut().pump_queue_to_socket(cx);
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpHalf {
|
||||
/// Send as many queued datagrams as the socket accepts. Returns
|
||||
/// `Ready(Ok(()))` when the queue is empty; WouldBlock registers
|
||||
/// the send waker (via `poll_send_ready`) and keeps the remainder
|
||||
/// queued for the next flush/wake.
|
||||
fn pump_queue_to_socket(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
use std::task::Poll;
|
||||
loop {
|
||||
let dg = match self
|
||||
.deframe
|
||||
.sending
|
||||
.take()
|
||||
.or_else(|| self.deframe.queue.pop_front())
|
||||
{
|
||||
Some(dg) => dg,
|
||||
None => return Poll::Ready(Ok(())), // queue drained
|
||||
};
|
||||
match self.sock.try_send(&dg) {
|
||||
Ok(_) => {
|
||||
self.deframe.accepted -= dg.len() + 2;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
self.deframe.sending = Some(dg);
|
||||
// Register the waker for the retry.
|
||||
return match self.sock.poll_send_ready(cx) {
|
||||
Poll::Ready(Ok(())) => Poll::Pending,
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
};
|
||||
}
|
||||
Err(e) => return Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Unix ---------------------------------------------------------------
|
||||
|
||||
/// Dial a unix-socket target (OQ-TN-14 — ships in v1, the same halves
|
||||
|
||||
Reference in new issue
Block a user