Files
alkssh/src/client_accept.rs
T
glm-5.3-flash 7e5989f574 feat(client): land the -R leg — the acceptor registry + the reverse-listen rows (w4-reverse-listen)
- src/client_accept.rs: the assembly-mutable acceptor registry
  (per-type fixed slots under `tunnel`; refuse-never-replace install;
  payload = the `TunnelChannel` duplex + the open's echo/originator
  fields — the harness `ForwardedAcceptor` vocabulary grown), the
  reverse-listen rows (bound-port surfaced; the listen/cancel forms;
  the runtime-Unix gate at the listen), and the accepted-opens'
  spawned handoff tasks.
- src/client_handler.rs: the `forwarded-tcpip` /
  `forwarded-streamlocal` hooks route through the registry at
  arrival time — registered + admitted → accept + handoff;
  unregistered/unfeatured/cap-exhausted → the clean
  `AdministrativelyProhibited` decline (never accept-and-drop);
  pre-install arrivals decline, never buffered.
- src/client.rs: the dial wires the empty registry into the handler
  + the connection (`SshConnection::acceptors`, the late-install
  accessor) + the listener budget's dedicated counter.
- src/error.rs: `OpError::NoAcceptor` / `ListenRefused` /
  `ListenCapExceeded`; `RequestDenied` → `ListenRefused` (the
  listen/cancel forms' wire decline class; the port-0 mint rides
  alone at this pin — an explicit port's bound *is* the requested
  one, surfaced).
- tests/common/sshd_double.rs: the fake `sshd`'s `-R` half — the
  listen/cancel hooks bind real sockets (the out-param mint is the
  real bound port), the accept loops open the server-initiated
  forwarded channels + pump per accepted connection, cancels match
  by the raw wire strings and tear the listener down.
- tests/client_reverse_listen.rs: the contract rows (listen + data
  leg, the mint, the pre-wire gates, the declines, late-install
  on/off, the cancels, the ceiling, the unix twins).
- tests/tunnel_mapping.rs: the full-circle client ↔ server-direction
  smoke (both `-R` legs through the alkcall fabric double); real-
  `sshd` `-R` stays wave-5 e2e (recorded).
- client.md: the acceptor's landing record (shape, cap relation,
  gates, unix-gate placement, port-0 parity).
- fix(w4-op-rows, test cfg): the test mod's `family_exit_code`
  import gated to the exit-consuming builds (`--features tunnel`
  did not compile its tests before).

Verification: cargo test (default / each feature / --all-features)
green; cargo clippy --all-targets -- -D warnings (none +
--all-features) clean; cargo fmt --check clean; cargo doc --no-deps
clean (default build; all-features carries only the pre-existing
client_ops private-link warnings).
2026-10-29 17:19:24 +00:00

625 lines
26 KiB
Rust

//! The `-R` leg's client half (`w4-reverse-listen` — `client.md`
//! §op rows row 4 + §The acceptor): the hub's reverse direction in
//! family types. Two surfaces, the acceptor registry first:
//!
//! 1. **The forward request rows** — [`SshConnection::reverse_listen`]
//! / [`SshConnection::reverse_listen_cancel`] / the unix pair
//! (`reverse_listen_unix` / `reverse_listen_unix_cancel`): the
//! vanilla-server listen requests, the lifecycle primitives
//! (`client.md` §Lifecycle — the actually-bound port surfaced,
//! every listener explicitly cancellable; the *handles*' surface is
//! `w4-lifecycle`'s growth).
//! 2. **The acceptor registry** ([`AcceptorRegistry`]) — the
//! assembly-mutable, per-channel-type registry the dial surface's
//! russh handler routes **server-initiated `forwarded-*` opens**
//! through. Served only under a registered acceptor for that type
//! and under the `tunnel` feature; unregistered/unfeatured accepted
//! opens **decline cleanly** — never russh's accept-and-drop
//! default (ADR-003 §4, the client-side fail-closed acceptor; the
//! reverse-runner fact 1 disposition is ours).
//!
//! ## The recorded decisions (the task's decide + record items)
//!
//! - **Registry shape: per-type fixed slots, not a typed map** — the
//! client direction's registry-servable type set is closed and tiny
//! (exactly the `-R` data leg's two: `forwarded-tcpip` /
//! `forwarded-streamlocal`, both under `tunnel`); one trait per type
//! ([`TcpipForwardedAcceptor`] / [`StreamlocalForwardedAcceptor`])
//! makes *which acceptor serves which type* a compile-time fact and
//! the fail-closed gate a one-slot read. A generic typed map would
//! add runtime dispatch (a typed key + an enum payload) for a set
//! that cannot grow without an ADR (any further server-initiated
//! type is a mapping, `client.md` §op rows). The harness's
//! `ForwardedAcceptor` vocabulary is grown, not forked: the payload
//! fields are the same shapes (the open's originator fields + the
//! channel duplex), the duplex now family-typed
//! ([`TunnelChannel`](crate::TunnelChannel) — the `-L` rows' byte
//! duplex, one type both legs ride).
//! - **Install is refuse-never-replace** ([`AcceptorRegistry::
//! install_tcpip`]'s held-slot error): the same posture recorded
//! server-side on the `ListenRouter` at `w4-router-collision` —
//! a same-slot second install is an assembly program error, declined
//! never a quiet displacement of a live acceptor. Uninstall is
//! explicit, 1:1 with the install.
//! - **Late-install across a live connection** (the alkminer
//! `install_forward_sink` dynamic — reverse-runner fact 2): the
//! registry rides the connected [`SshConnection`]
//! ([`SshConnection::acceptors`]) — the dial constructs it empty,
//! the assembly installs/uninstalls whenever it decides. The open
//! hooks read the slot **at arrival time**, fail-closed: an open
//! arriving before its acceptor installs (or after an uninstall)
//! is **declined at arrival, never buffered** — no unclaimed-open
//! queue exists anywhere in this module's posture (an unclaimed
//! open's bytes would have to be buffered unboundedly to accept-
//! then-route; the decline is the clean, bounded answer).
//! - **The listen rows gate on the registered acceptor** — the
//! *issuing*-side disposition: a listen whose data leg cannot serve
//! is a half-capability (the peer stays bound, every arrival
//! declines), so a `reverse_listen` with no registered acceptor for
//! the type answers
//! [`OpError::NoAcceptor`](crate::OpError) **before any wire op**
//! (install-first is the recorded order; the rows never buffer an
//! outstanding listen against a later install — re-issuing is the
//! assembly's retry, the listen rows are stateless global requests).
//! - **Bounded bookkeeping (AGENTS.md convention 9) — the cap
//! relation** (the server-side listener-counter posture as the
//! template): the registry is bounded by construction (two fixed
//! slots). The per-connection **listener count** is a dedicated
//! in-crate counter admitting at the listen rows and releasing at
//! the confirmed cancels and the failed/wire-error paths — bounded
//! against the *same* `Limits::max_concurrent_channels` ceiling the
//! server direction's listener counter uses (independent counts, one
//! ceiling — never a map: the outstanding listens live at the peer;
//! the counter exists so a dial cannot mint an unbounded listener
//! set the registry would have to serve). The per-connection
//! **accepted-open handoff count** is the second dedicated counter
//! against the same ceiling (admitted in the dial handler's open
//! hook, accepted-with-the-count-held, released when the acceptor's
//! `accept` call completes — no accepted-open ledger anywhere
//! else: the pump is the acceptor's from the handoff on, the
//! channel's transport bounds are russh's windows, and the slot read
//! is the arrival admission).
//! - **The runtime-Unix gate (OQ-11 symmetry)**: the streamlocal
//! *listen* rows (`reverse_listen_unix` / `reverse_listen_unix_cancel`)
//! compile everywhere and
//! decline off-Unix before any wire op (the bind is the
//! platform-bound operation — the door puts nothing on the wire it
//! cannot track). The `forwarded-streamlocal` *open* hook is
//! universal: ingestion is platform-neutral byte pumping — the
//! unix-socket leg's socket is the peer-side's.
//! - **`OpError::ListenRefused`** — the peer's `RequestDenied` on
//! every form's decline (a denied listen, a cancel of an unknown
//! listener) — channel-scoped honesty: the connection stays usable
//! (the `From` mapping's recorded producer, `error.rs`).
//!
//! ## The data leg
//!
//! Per accepted open: the hook accepts the channel and hands the
//! assembly the open — the [`TunnelChannel`](crate::TunnelChannel)
//! duplex + the open's echo/originator fields (the payload types
//! below) — on a spawned task (the spawn-and-return rule's client
//! mirror: the hook runs in the packet loop and returns fast). The
//! pump is the acceptor's from the handoff — the family handler shape
//! (alktunnels' `ForwardedHandler` recipe, POC-3 §C.4); dropping the
//! duplex is wire-clean (russh closes the channel).
//!
//! ## Test surface
//!
//! The wire vehicle is the fake `sshd` double grown with the
//! server-initiated-forward surfaces (`tests/common/sshd_double.rs`) —
//! the double binds **real** sockets for its script accepts, opens the
//! forwarded channels toward the client per accepted connection, and
//! pumps them (a real `ssh -R`-shaped round trip in-process, the task's
//! recorded smoke posture). The full-circle alkssh-client ↔
//! alkssh-server-direction smoke (both `-R` legs through the alkcall
//! fabric, the composition the assembly makes) rides the fabric
//! doubles in `tests/tunnel_mapping.rs` — the same doubles the
//! server-side `-R` rows use; real-`sshd` `-R` is wave-5 e2e
//! territory, never silently dropped.
//!
//! ## Smoke posture (recorded)
//!
//! The in-process smoke shape is a real `ssh -R`-shaped round trip: a
//! TCP connection to the forward's actually-bound port round-trips
//! through the registered acceptor's duplex (the reverse-runner's
//! hub-side proxy shape). The full-circle variant against the alkssh
//! server direction pins the client registry against the server
//! direction's real arrival pump (the echo/originator/mint shapes
//! `w3-tunnel-listen` landed); real-`sshd` `-R` is wave-5 e2e.
use std::sync::Arc;
use async_trait::async_trait;
use parking_lot::Mutex;
use russh::client::ChannelOpenHandle;
use crate::TunnelChannel;
use crate::channel_cap::ChannelCap;
use crate::client::SshConnection;
use crate::error::OpError;
use crate::observe;
/// An accepted server-initiated `forwarded-tcpip` open — the acceptor
/// registry's payload for the
/// [`TcpipForwardedAcceptor::accept`] call: the open's echo fields
/// (the listen endpoint the arrival belongs to — the assembly's
/// routing key; the OpenSSH matcher parity verbatim) + the originator
/// (the arrival's peer, as the wire carried it) + the channel duplex.
pub struct ForwardedTcpip {
/// The connected address the open echoed — the `reverse_listen`
/// wire string (sshd parity).
pub connected_address: String,
/// The actually-bound port of the minted listener — for a
/// `port = 0` mint this is the server-picked number, never `0`.
pub connected_port: u32,
/// The arrival's peer address (the wire originator).
pub originator_address: String,
/// The arrival's peer port (the wire originator).
pub originator_port: u32,
/// The accepted channel's byte duplex — the acceptor owns the
/// pump from the handoff on (dropping it is wire-clean).
pub stream: TunnelChannel,
}
impl std::fmt::Debug for ForwardedTcpip {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForwardedTcpip")
.field("connected_address", &self.connected_address)
.field("connected_port", &self.connected_port)
.field("originator_address", &self.originator_address)
.field("originator_port", &self.originator_port)
.finish_non_exhaustive()
}
}
/// An accepted server-initiated `forwarded-streamlocal` open — the
/// streamlocal acceptor's payload (the echoed socket path + the
/// channel duplex).
pub struct ForwardedStreamlocal {
/// The socket path the open echoed — the `reverse_listen_unix`
/// wire string (sshd parity).
pub socket_path: String,
/// The accepted channel's byte duplex — the acceptor owns the
/// pump from the handoff on (dropping it is wire-clean).
pub stream: TunnelChannel,
}
impl std::fmt::Debug for ForwardedStreamlocal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForwardedStreamlocal")
.field("socket_path", &self.socket_path)
.finish_non_exhaustive()
}
}
/// The `forwarded-tcpip` acceptor — the registry's per-type slot
/// trait. `accept` runs on the spawned handoff task (never the
/// packet loop): the assembly's pump/home of the open lives inside
/// it; the crate releases its handoff counter when the call returns,
/// so the family shape (hand off to a queue, return) keeps the
/// counter's meaning "open accepted, handoff in flight".
#[async_trait]
pub trait TcpipForwardedAcceptor: Send + Sync {
/// Serves one accepted open. Never invoked for an unregistered
/// type (the registry gate is first); the open is already accepted
/// on the wire — refusing one from here is a *data-plane* choice
/// (drop the duplex; the channel closes on it), never a wire
/// open-decline.
async fn accept(&self, open: ForwardedTcpip);
}
/// The `forwarded-streamlocal` acceptor — the unix slot's trait
/// (see [`TcpipForwardedAcceptor`] for the call-shape contract).
#[async_trait]
pub trait StreamlocalForwardedAcceptor: Send + Sync {
/// Serves one accepted open (platform-neutral pumping — see the
/// module docs' unix-gate record).
async fn accept(&self, open: ForwardedStreamlocal);
}
/// The held-slot refusal (`AcceptorRegistry::install_*`) — the
/// refuse-never-replace posture (the server-side `ListenRouter`'s
/// recorded sibling): the slot already holds a live acceptor; a second
/// install is the assembly's program error, declined, never a quiet
/// displacement. An explicit `uninstall_*` frees the slot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("an acceptor is already registered for this channel type")]
pub struct AcceptorHeld;
/// The assembly-mutable, per-channel-type acceptor registry — the
/// `-R` data leg's fail-closed gate (see the module docs for the
/// recorded decisions). Constructed empty at the dial
/// ([`SshConnection::acceptors`], the live-connection accessor);
/// install/uninstall is the assembly's to drive whenever it decides.
pub struct AcceptorRegistry {
tcpip: Mutex<Option<Arc<dyn TcpipForwardedAcceptor>>>,
streamlocal: Mutex<Option<Arc<dyn StreamlocalForwardedAcceptor>>>,
}
impl AcceptorRegistry {
/// The empty registry — the dial's construct (nothing is served
/// until an acceptor installs; ADR-003 §4's fail-closed default).
pub fn new() -> Self {
Self {
tcpip: Mutex::new(None),
streamlocal: Mutex::new(None),
}
}
/// Installs the `forwarded-tcpip` acceptor. Refuse-never-replace:
/// a held slot declines ([`AcceptorHeld`]) — uninstall first if a
/// replacement is meant (the late-install toggle's other half).
pub fn install_tcpip(
&self,
acceptor: Arc<dyn TcpipForwardedAcceptor>,
) -> Result<(), AcceptorHeld> {
let mut slot = self.tcpip.lock();
if slot.is_some() {
return Err(AcceptorHeld);
}
*slot = Some(acceptor);
Ok(())
}
/// Uninstalls the `forwarded-tcpip` acceptor — `true` when one was
/// held (the toggle's honest report). In-flight opens already
/// accepted keep pumping (their handoff happened); arrivals after
/// this decline at arrival time (fail-closed, no buffering).
pub fn uninstall_tcpip(&self) -> bool {
self.tcpip.lock().take().is_some()
}
/// Installs the `forwarded-streamlocal` acceptor (see
/// [`Self::install_tcpip`] for the refuse-never-replace posture).
pub fn install_streamlocal(
&self,
acceptor: Arc<dyn StreamlocalForwardedAcceptor>,
) -> Result<(), AcceptorHeld> {
let mut slot = self.streamlocal.lock();
if slot.is_some() {
return Err(AcceptorHeld);
}
*slot = Some(acceptor);
Ok(())
}
/// Uninstalls the `forwarded-streamlocal` acceptor (see
/// [`Self::uninstall_tcpip`] for the honest-report posture).
pub fn uninstall_streamlocal(&self) -> bool {
self.streamlocal.lock().take().is_some()
}
/// The arrival-time slot reads (the open hooks' gates) — cloned
/// out so the spawned handoff task owns its acceptor handle.
pub(crate) fn tcpip_acceptor(&self) -> Option<Arc<dyn TcpipForwardedAcceptor>> {
self.tcpip.lock().clone()
}
pub(crate) fn streamlocal_acceptor(&self) -> Option<Arc<dyn StreamlocalForwardedAcceptor>> {
self.streamlocal.lock().clone()
}
}
impl Default for AcceptorRegistry {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for AcceptorRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcceptorRegistry")
.field("tcpip", &self.tcpip.lock().is_some())
.field("streamlocal", &self.streamlocal.lock().is_some())
.finish()
}
}
// ---------------------------------------------------------------------------
// The server-initiated opens' handoff (the dial handler's routed paths)
// ---------------------------------------------------------------------------
/// The accepted `forwarded-tcpip` open's handoff task — the
/// spawn-and-return rule's client mirror (ADR-006 decision 4; the
/// hook runs in the packet loop and returns fast): accepts the open on
/// the wire, converts the channel to the family duplex, delivers the
/// payload to the assembly's acceptor, and releases the handoff
/// counter when the acceptor's call completes (the recorded release
/// site — the count's meaning is "accepted, handoff in flight").
#[cfg_attr(feature = "tunnel", allow(clippy::too_many_arguments))]
pub(crate) async fn handoff_forwarded_tcpip(
acceptor: Arc<dyn TcpipForwardedAcceptor>,
open_cap: Arc<ChannelCap>,
channel: russh::Channel<russh::client::Msg>,
reply: ChannelOpenHandle,
connected_address: String,
connected_port: u32,
originator_address: String,
originator_port: u32,
) {
let channel_number = channel.id().number();
observe::serving_path_started("forwarded-tcpip", channel_number, None);
reply.accept().await;
let open = ForwardedTcpip {
connected_address,
connected_port,
originator_address,
originator_port,
stream: TunnelChannel {
stream: channel.into_stream(),
},
};
acceptor.accept(open).await;
open_cap.release();
observe::serving_path_ended("forwarded-tcpip", channel_number, None);
}
/// The accepted `forwarded-streamlocal` open's handoff task — the
/// unix slot's mirror of [`handoff_forwarded_tcpip`].
pub(crate) async fn handoff_forwarded_streamlocal(
acceptor: Arc<dyn StreamlocalForwardedAcceptor>,
open_cap: Arc<ChannelCap>,
channel: russh::Channel<russh::client::Msg>,
reply: ChannelOpenHandle,
socket_path: String,
) {
let channel_number = channel.id().number();
observe::serving_path_started("forwarded-streamlocal", channel_number, None);
reply.accept().await;
let open = ForwardedStreamlocal {
socket_path,
stream: TunnelChannel {
stream: channel.into_stream(),
},
};
acceptor.accept(open).await;
open_cap.release();
observe::serving_path_ended("forwarded-streamlocal", channel_number, None);
}
// ---------------------------------------------------------------------------
// The reverse-listen rows (under `tunnel`)
// ---------------------------------------------------------------------------
impl SshConnection {
/// The reverse-listen row (`-R`): asks the vanilla server to listen
/// on (address, port) and returns the **actually-bound port** —
/// for `port = 0` this is the peer-picked number (`client.md`
/// §Lifecycle: the bound-port primitive, the port-0 mint); for an
/// explicit port the wire's success reply carries no number (russh/
/// sshd parity — the reply's data only exists for mints) and the
/// bound port *is* the requested one, which the row surfaces.
///
/// Gates, both **before any wire op**: the `forwarded-tcpip`
/// acceptor must be registered
/// ([`OpError::NoAcceptor`](crate::OpError) — a listen whose data
/// leg cannot serve is a half-capability) and the connection's
/// listener count must have room
/// ([`OpError::ListenCapExceeded`](crate::OpError) — the
/// dedicated counter sharing
/// [`Limits::max_concurrent_channels`](crate::Limits); see the
/// module docs' cap relation). The peer's refusal is
/// [`OpError::ListenRefused`](crate::OpError) (the wire's
/// `RequestDenied`) and releases the count; the connection stays
/// usable after every refusal class. The listener is explicitly
/// cancellable ([`SshConnection::reverse_listen_cancel`]; it is
/// not implicitly unlistened by anything but the connection's
/// end).
pub async fn reverse_listen(
&self,
address: impl Into<String>,
port: u32,
) -> Result<u32, OpError> {
if self.acceptors().tcpip_acceptor().is_none() {
return Err(OpError::NoAcceptor);
}
if !self
.listener_budget()
.admit(self.limits().max_concurrent_channels)
{
return Err(OpError::ListenCapExceeded);
}
let bound = match self.russh_handle().tcpip_forward(address, port).await {
Ok(minted) if port == 0 => minted,
Ok(_) => port,
Err(error) => {
self.listener_budget().release();
return Err(OpError::from(error));
}
};
Ok(bound)
}
/// The reverse-listen cancel row (`-R` teardown): the explicit
/// cancellation of one bound listener (the lifecycle primitive:
/// every listener explicitly cancellable, the wire-honest path —
/// the peer tears its listen down, the bound port frees). Matches
/// by the raw wire strings (sshd parity). The peer's refusal
/// (unknown listener) is [`OpError::ListenRefused`](crate::OpError);
/// a confirmed cancel releases the listener count.
pub async fn reverse_listen_cancel(
&self,
address: impl Into<String>,
port: u32,
) -> Result<(), OpError> {
match self
.russh_handle()
.cancel_tcpip_forward(address, port)
.await
{
Ok(()) => {
self.listener_budget().release();
Ok(())
}
Err(error) => Err(OpError::from(error)),
}
}
/// The streamlocal reverse-listen row (the unix `-R`): asks the
/// vanilla server to listen on the unix socket at `socket_path`,
/// minting no port (the global request's success is the outcome;
/// the accepted opens' echo carries the path). Same gates as
/// [`SshConnection::reverse_listen`] for the
/// `forwarded-streamlocal` slot, plus the **runtime-Unix gate**
/// (OQ-11: off-Unix, [`OpError::UnixUnavailable`](crate::OpError)
/// before any wire op — the bind is the platform-bound operation;
/// the channel ingestion is platform-neutral).
#[cfg_attr(not(unix), allow(unused_variables))]
pub async fn reverse_listen_unix(&self, socket_path: impl Into<String>) -> Result<(), OpError> {
#[cfg(not(unix))]
{
return Err(OpError::UnixUnavailable);
}
#[cfg(unix)]
{
if self.acceptors().streamlocal_acceptor().is_none() {
return Err(OpError::NoAcceptor);
}
if !self
.listener_budget()
.admit(self.limits().max_concurrent_channels)
{
return Err(OpError::ListenCapExceeded);
}
match self.russh_handle().streamlocal_forward(socket_path).await {
Ok(()) => Ok(()),
Err(error) => {
self.listener_budget().release();
Err(OpError::from(error))
}
}
}
}
/// The streamlocal cancel row (matches by the socket path, sshd
/// parity — see [`SshConnection::reverse_listen_cancel`]).
#[cfg_attr(not(unix), allow(unused_variables))]
pub async fn reverse_listen_unix_cancel(
&self,
socket_path: impl Into<String>,
) -> Result<(), OpError> {
#[cfg(not(unix))]
{
return Err(OpError::UnixUnavailable);
}
#[cfg(unix)]
{
match self
.russh_handle()
.cancel_streamlocal_forward(socket_path)
.await
{
Ok(()) => {
self.listener_budget().release();
Ok(())
}
Err(error) => Err(OpError::from(error)),
}
}
}
}
#[cfg(test)]
mod tests {
use super::AcceptorHeld;
use crate::client_accept::{
AcceptorRegistry, ForwardedStreamlocal, ForwardedTcpip, StreamlocalForwardedAcceptor,
TcpipForwardedAcceptor,
};
use std::sync::Arc;
struct StubTcpip;
#[async_trait::async_trait]
impl TcpipForwardedAcceptor for StubTcpip {
async fn accept(&self, _open: ForwardedTcpip) {}
}
struct StubStreamlocal;
#[async_trait::async_trait]
impl StreamlocalForwardedAcceptor for StubStreamlocal {
async fn accept(&self, _open: ForwardedStreamlocal) {}
}
/// - *Contract stamp:* the recorded refuse-never-replace posture —
/// a held slot declines the second install (never a quiet
/// displacement of a live acceptor); uninstall frees it.
#[test]
fn install_refuses_a_held_slot_and_uninstall_frees_it() {
let registry = AcceptorRegistry::new();
let first: Arc<dyn TcpipForwardedAcceptor> = Arc::new(StubTcpip);
assert!(registry.install_tcpip(Arc::clone(&first)).is_ok());
assert!(
registry.install_tcpip(Arc::new(StubTcpip)).is_err(),
"a held slot is refused, never replaced"
);
let held = registry.tcpip_acceptor().expect("the first stays held");
assert!(Arc::ptr_eq(&held, &first));
assert!(registry.uninstall_tcpip());
assert!(
!registry.uninstall_tcpip(),
"the second uninstall is the honest `false`"
);
assert!(registry.tcpip_acceptor().is_none());
assert!(
registry.install_tcpip(first).is_ok(),
"an uninstalled slot installs again (the late-install toggle)"
);
}
/// - *Contract stamp:* the two slots are independent (the
/// per-type-registry decision) — the empty streamlocal slot
/// stays unfeatured while the tcpip slot serves.
#[test]
fn the_slots_are_independent() {
let registry = AcceptorRegistry::new();
assert!(registry.tcpip_acceptor().is_none());
assert!(registry.streamlocal_acceptor().is_none());
registry
.install_tcpip(Arc::new(StubTcpip))
.expect("the tcp slot installs");
assert!(registry.tcpip_acceptor().is_some());
assert!(
registry.streamlocal_acceptor().is_none(),
"the streamlocal slot is untouched"
);
registry
.install_streamlocal(Arc::new(StubStreamlocal))
.expect("the streamlocal slot installs independently");
assert!(registry.streamlocal_acceptor().is_some());
}
/// - *Contract stamp:* the Debug shape (the installed-slot fact,
/// never internals).
#[test]
fn the_registry_debug_shapes_the_slots() {
let registry = AcceptorRegistry::new();
let debug = format!("{registry:?}");
assert!(
debug.contains("tcpip: false") && debug.contains("streamlocal: false"),
"{debug}"
);
registry
.install_tcpip(Arc::new(StubTcpip))
.expect("installs");
let debug = format!("{registry:?}");
assert!(debug.contains("tcpip: true"), "{debug}");
}
/// - *Contract stamp:* the held-slot error's honest shape (no
/// `Other`, no catch-all).
#[test]
fn acceptor_held_error_shapes() {
let held = AcceptorHeld;
assert_eq!(
held.to_string(),
"an acceptor is already registered for this channel type"
);
}
}