Skip to main content

logicaffeine_system/
tcp.rs

1//! TCP client natives (tcp.lg): the SYNC implementations the AOT tier calls via
2//! `map_native_function`, plus the shared error-message formatters every tier
3//! reuses — one wording, so the tiers cannot drift even in their `Err` text.
4//!
5//! Std-only (no tokio): the AOT tier blocks the calling thread, exactly like
6//! `file::read`. The async tiers (tree-walker/VM) live in the compile crate's
7//! `TcpRegistry` over tokio and reuse the formatters below.
8//!
9//! Connections are process-global, keyed by an opaque `Int` handle — the value a
10//! LOGOS program threads through `tcpSend`/`tcpRecv`/`tcpClose`. A closed or
11//! never-opened handle is always an `Err` value, never a panic.
12
13use std::collections::HashMap;
14use std::io::{Read, Write};
15use std::net::TcpStream;
16use std::sync::{Arc, Mutex, OnceLock};
17
18use logicaffeine_data::LogosSeq;
19
20/// The largest single `tcpRecv`/`tcpRecvExact` request (64 MiB) — a guard against
21/// a hostile or corrupted length field, not a protocol limit (loop for more).
22pub const MAX_RECV: i64 = 64 * 1024 * 1024;
23
24pub fn err_invalid_port(port: i64) -> String {
25    format!("tcp: invalid port {port} (1..=65535)")
26}
27pub fn err_connect(host: &str, port: i64, e: &dyn std::fmt::Display) -> String {
28    format!("tcp: connect to {host}:{port} failed: {e}")
29}
30pub fn err_unknown_conn(id: i64) -> String {
31    format!("tcp: closed or unknown connection {id}")
32}
33pub fn err_invalid_len(len: i64) -> String {
34    format!("tcp: invalid length {len} (0..={MAX_RECV})")
35}
36pub fn err_send(e: &dyn std::fmt::Display) -> String {
37    format!("tcp: send failed: {e}")
38}
39pub fn err_recv(e: &dyn std::fmt::Display) -> String {
40    format!("tcp: recv failed: {e}")
41}
42pub fn err_torn(got: usize, want: i64) -> String {
43    format!("tcp: connection closed after {got} of {want} bytes")
44}
45pub fn err_timeout() -> String {
46    "tcp: operation timed out".to_string()
47}
48pub fn err_listen(port: i64, e: &dyn std::fmt::Display) -> String {
49    format!("tcp: listen on port {port} failed: {e}")
50}
51pub fn err_accept(e: &dyn std::fmt::Display) -> String {
52    format!("tcp: accept failed: {e}")
53}
54pub fn err_shutdown(e: &dyn std::fmt::Display) -> String {
55    format!("tcp: shutdown failed: {e}")
56}
57pub fn err_invalid_timeout(millis: i64) -> String {
58    format!("tcp: invalid timeout {millis} ms (0 clears, otherwise 1..)")
59}
60
61/// True when an IO error is the stream's read/write deadline expiring.
62fn is_timeout(e: &std::io::Error) -> bool {
63    matches!(
64        e.kind(),
65        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
66    )
67}
68
69struct Registry {
70    /// ★ EACH CONNECTION CARRIES ITS OWN LOCK, and the registry's lock is held only long enough
71    /// to look one up. Holding the REGISTRY lock across a blocking syscall meant one stalled peer
72    /// froze every other connection — and froze `close`/`set_timeout`/`shutdown_write` too, so the
73    /// calls used to CANCEL a hang queued behind the hang. `accept` already knew this ("Lock per
74    /// poll so a long accept never wedges other connections"); the other three did not.
75    ///
76    /// Per-connection rather than a cloned handle because a future TLS stream is a stateful
77    /// SESSION that cannot be duplicated: two operations on one connection must serialize, two
78    /// on different connections must not.
79    conns: HashMap<i64, ConnEntry>,
80    listeners: HashMap<i64, std::net::TcpListener>,
81    /// Accept timeout per listener — std has no accept deadline, so a timed
82    /// listener is switched non-blocking and polled against the deadline.
83    listener_timeouts: HashMap<i64, std::time::Duration>,
84    next: i64,
85}
86
87/// One connection: the stream its I/O serializes on, and a SEPARATE handle for control.
88///
89/// ★ THE CONTROL HANDLE IS THE POINT. `set_timeout`, `shutdown_write` and `close` are how a
90/// program cancels a hung connection, so they must not queue behind the very read they are
91/// cancelling. `try_clone` duplicates the descriptor — same socket, independent handle — so a
92/// deadline or a FIN can be applied while a reader is parked inside `read`. Closing shuts the
93/// socket down through it, which is what actually wakes the blocked reader rather than merely
94/// dropping a map entry the reader still holds an `Arc` to.
95struct ConnEntry {
96    io: Arc<Mutex<Conn>>,
97    ctl: TcpStream,
98}
99
100/// A connection's byte stream: plain, or wrapped in a TLS session.
101///
102/// ★ THE HANDLE STAYS THE SAME, WHICH IS THE WHOLE DESIGN. `tcpSend`/`tcpRecv`/`tcpRecvExact`
103/// dispatch on this and nothing above them changes — so `postgres.lg`, two thousand lines of pure
104/// LOGOS speaking the wire over these natives with SCRAM-SHA-256 included, gains TLS without one
105/// line changing. The same is true of anything else ever written over TCP.
106///
107/// A TLS session is STATEFUL and cannot be duplicated, which is why the entry above serializes I/O
108/// on its own lock and keeps a separate raw descriptor for control: a deadline or a FIN still
109/// applies while a reader is parked, but two reads of one session can never interleave.
110enum Conn {
111    Plain(TcpStream),
112    Tls(Box<rustls::StreamOwned<rustls::ClientConnection, TcpStream>>),
113}
114
115impl Read for Conn {
116    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
117        match self {
118            Conn::Plain(s) => s.read(buf),
119            Conn::Tls(s) => s.read(buf),
120        }
121    }
122}
123
124impl Write for Conn {
125    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
126        match self {
127            Conn::Plain(s) => s.write(buf),
128            Conn::Tls(s) => s.write(buf),
129        }
130    }
131    fn flush(&mut self) -> std::io::Result<()> {
132        match self {
133            Conn::Plain(s) => s.flush(),
134            Conn::Tls(s) => s.flush(),
135        }
136    }
137}
138
139fn registry() -> &'static Mutex<Registry> {
140    static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
141    REGISTRY.get_or_init(|| {
142        Mutex::new(Registry {
143            conns: HashMap::new(),
144            listeners: HashMap::new(),
145            listener_timeouts: HashMap::new(),
146            next: 1,
147        })
148    })
149}
150
151/// The connection's own lock, looked up under the registry lock and returned so the caller can do
152/// its blocking I/O with the registry FREE. This is the whole fix for the wedging described on
153/// [`Registry::conns`].
154fn conn_handle(conn: i64) -> Result<Arc<Mutex<Conn>>, String> {
155    let reg = registry().lock().unwrap();
156    reg.conns
157        .get(&conn)
158        .map(|e| e.io.clone())
159        .ok_or_else(|| err_unknown_conn(conn))
160}
161
162/// Wrap an accepted or connected stream, duplicating its descriptor for the control handle.
163fn new_entry(stream: TcpStream) -> Result<ConnEntry, String> {
164    let ctl = stream.try_clone().map_err(|e| err_recv(&e))?;
165    Ok(ConnEntry {
166        io: Arc::new(Mutex::new(Conn::Plain(stream))),
167        ctl,
168    })
169}
170
171fn seq_to_bytes(data: &[i64]) -> Vec<u8> {
172    data.iter().map(|v| (v & 0xff) as u8).collect()
173}
174
175fn bytes_to_seq(bytes: Vec<u8>) -> LogosSeq<i64> {
176    LogosSeq::from_vec(bytes.into_iter().map(|b| b as i64).collect())
177}
178
179/// Open a TCP connection; `Ok` carries the opaque connection handle.
180pub fn connect(host: String, port: i64) -> Result<i64, String> {
181    if !(1..=65535).contains(&port) {
182        return Err(err_invalid_port(port));
183    }
184    match TcpStream::connect((host.as_str(), port as u16)) {
185        Ok(stream) => {
186            // Interactive protocol clients disable Nagle (libpq/tokio-postgres do) —
187            // small request/response frames must not wait for coalescing.
188            stream
189                .set_nodelay(true)
190                .map_err(|e| err_connect(&host, port, &e))?;
191            let mut reg = registry().lock().unwrap();
192            let id = reg.next;
193            reg.next += 1;
194            reg.conns.insert(id, new_entry(stream)?);
195            Ok(id)
196        }
197        Err(e) => Err(err_connect(&host, port, &e)),
198    }
199}
200
201/// The client TLS configuration: the bundled Mozilla roots, plus any extra root the operator
202/// names in `LOGOS_TLS_EXTRA_ROOT` (a PEM file).
203///
204/// ★ PUBLIC BECAUSE BOTH TIERS MUST SHARE IT. The interpreter and VM drive sockets through tokio
205/// and the AOT tier drives them synchronously, so there are two socket registries — but there must
206/// be exactly ONE set of trusted roots and one verification policy. Two copies would drift, and the
207/// direction they drift in is "which certificates are accepted".
208///
209/// ★ BUNDLED ROOTS, NOT THE OS STORE, so a compiled binary trusts the same authorities wherever it
210/// runs — the same reason every other input here is pinned. An OS store makes a build's behaviour
211/// depend on the machine it happens to execute on.
212///
213/// ★ AND THE EXTRA ROOT IS A REAL FEATURE, not test scaffolding: a private CA is how a
214/// self-hosted PostgreSQL or an internal API is reached, and without a seam for it those are
215/// simply unreachable. It also lets the socket tests stand up a hermetic TLS server rather than
216/// proving TLS against the internet.
217/// Install the process-level rustls `CryptoProvider`, once, idempotently.
218///
219/// ★ IT HAS TO BE CALLABLE FROM BOTH SIDES, and it used to be reachable only from one.
220///
221/// rustls refuses to guess when more than one crypto backend is compiled in and PANICS at the
222/// first handshake — and Cargo feature unification makes that unavoidable by declaration alone,
223/// since another crate in this workspace's graph enables `aws-lc-rs` and a feature enabled
224/// anywhere is enabled everywhere.
225///
226/// The install used to live inside [`tls_config`]'s cache, which only runs on the CLIENT path. A
227/// process that builds a `ServerConfig` first therefore panicked. Under `cargo test` that never
228/// showed: every test shares one process, so whichever test connected first installed the provider
229/// and every later server builder inherited it. Under nextest — one process PER TEST — the test
230/// that only ever builds a server got a process with no provider, and died. The ordering was doing
231/// the work, and the ordering is not a guarantee.
232pub fn ensure_crypto_provider() {
233    static ONCE: std::sync::Once = std::sync::Once::new();
234    ONCE.call_once(|| {
235        // Errors only if a provider is ALREADY set, which is a fine outcome.
236        let _ = rustls::crypto::ring::default_provider().install_default();
237    });
238}
239
240pub fn tls_config() -> Result<Arc<rustls::ClientConfig>, String> {
241    // ★ CACHED BY THE ROOTS IT WAS BUILT FROM, not once for the life of the process.
242    //
243    // A `OnceLock` froze whichever root set the FIRST connection happened to see. A program that
244    // reaches two databases behind two different private authorities then had its second
245    // `LOGOS_TLS_EXTRA_ROOT` silently ignored — the connection either fails with `UnknownIssuer`
246    // for no visible reason, or, worse, the reader concludes the variable does nothing and reaches
247    // for a way to skip verification. Keying the cache on the variable's current value costs one
248    // string comparison per connect and makes the setting mean what it says every time.
249    //
250    // It is also what makes `tcp_tls_lock` deterministic: one of its tests must trust the test CA
251    // and another must trust nothing, and under a threaded runner the `OnceLock` gave whichever
252    // ran first the power to decide for both. The lock's own doc comment predicted that race; it
253    // is fixed here rather than worked around there, because the product had the same bug.
254    static CONFIGS: OnceLock<Mutex<HashMap<String, Result<Arc<rustls::ClientConfig>, String>>>> =
255        OnceLock::new();
256    let key = std::env::var("LOGOS_TLS_EXTRA_ROOT").unwrap_or_default();
257    let cache = CONFIGS.get_or_init(|| Mutex::new(HashMap::new()));
258    if let Some(hit) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
259        return hit.clone();
260    }
261    let built = (|| {
262        // ★ CHOOSE THE PROVIDER EXPLICITLY. rustls refuses to guess when more than one crypto
263        // backend is compiled in, and panics at the first handshake with "Could not
264        // automatically determine the process-level CryptoProvider". Cargo FEATURE UNIFICATION
265        // makes that unavoidable by declaration alone: some other crate in this workspace's
266        // graph enables `aws-lc-rs`, and a feature enabled anywhere is enabled everywhere, so
267        // `default-features = false` on our own dependency cannot switch it off.
268        //
269        // Installing it here rather than at every entry point means the choice is made once,
270        // in the same place the roots are decided, and cannot be forgotten by a new caller.
271        // `install_default` errors only if a provider is ALREADY set, which is a fine outcome.
272        ensure_crypto_provider();
273        let mut roots = rustls::RootCertStore::empty();
274        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
275        if !key.is_empty() {
276            let path = key.clone();
277            let pem = std::fs::read(&path)
278                .map_err(|e| format!("tls: cannot read LOGOS_TLS_EXTRA_ROOT '{path}': {e}"))?;
279            let mut added = 0usize;
280            for der in rustls_pemfile_certs(&pem) {
281                roots
282                    .add(der)
283                    .map_err(|e| format!("tls: '{path}' is not a usable certificate: {e}"))?;
284                added += 1;
285            }
286            if added == 0 {
287                return Err(format!("tls: '{path}' contains no CERTIFICATE block"));
288            }
289        }
290        Ok(Arc::new(
291            rustls::ClientConfig::builder()
292                .with_root_certificates(roots)
293                .with_no_client_auth(),
294        ))
295    })();
296    cache
297        .lock()
298        .unwrap_or_else(|e| e.into_inner())
299        .insert(key, built.clone());
300    built
301}
302
303/// Every `CERTIFICATE` block in a PEM file, as DER. Written here rather than pulling a crate for
304/// it: the format is a base64 body between two marker lines.
305fn rustls_pemfile_certs(pem: &[u8]) -> Vec<rustls::pki_types::CertificateDer<'static>> {
306    const BEGIN: &str = "-----BEGIN CERTIFICATE-----";
307    const END: &str = "-----END CERTIFICATE-----";
308    let text = String::from_utf8_lossy(pem);
309    let mut out = Vec::new();
310    let mut rest = text.as_ref();
311    while let Some(b) = rest.find(BEGIN) {
312        let after = &rest[b + BEGIN.len()..];
313        let Some(e) = after.find(END) else { break };
314        let body: String = after[..e].chars().filter(|c| !c.is_whitespace()).collect();
315        if let Some(der) = base64_decode(&body) {
316            out.push(rustls::pki_types::CertificateDer::from(der));
317        }
318        rest = &after[e + END.len()..];
319    }
320    out
321}
322
323/// Standard base64 decode (no padding-length validation beyond what the alphabet implies).
324fn base64_decode(s: &str) -> Option<Vec<u8>> {
325    const A: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
326    let mut acc: u32 = 0;
327    let mut bits = 0u32;
328    let mut out = Vec::new();
329    for c in s.bytes() {
330        if c == b'=' {
331            break;
332        }
333        let v = A.iter().position(|&a| a == c)? as u32;
334        acc = (acc << 6) | v;
335        bits += 6;
336        if bits >= 8 {
337            bits -= 8;
338            out.push((acc >> bits) as u8);
339        }
340    }
341    Some(out)
342}
343
344/// Open a TCP connection and complete a TLS handshake over it; `Ok` carries the same opaque
345/// handle every other native takes, so `tcpSend`/`tcpRecv` work unchanged.
346pub fn connect_tls(host: String, port: i64) -> Result<i64, String> {
347    let id = connect(host.clone(), port)?;
348    match upgrade_tls(id, host) {
349        Ok(()) => Ok(id),
350        Err(e) => {
351            let _ = close(id);
352            Err(e)
353        }
354    }
355}
356
357/// Wrap an ESTABLISHED plain connection in TLS, in place.
358///
359/// ★ THIS IS THE ONE PostgreSQL NEEDS. Its negotiation is not "connect with TLS": the client sends
360/// an 8-byte SSLRequest over the plain socket, reads a single `S` or `N`, and only then hands the
361/// same socket to TLS. A connect-only API cannot express that. It also gives SMTP STARTTLS and
362/// HTTP `CONNECT` proxying for nothing.
363pub fn upgrade_tls(conn: i64, host: String) -> Result<(), String> {
364    let cfg = tls_config()?;
365    let server = rustls::pki_types::ServerName::try_from(host.clone())
366        .map_err(|_| format!("tls: '{host}' is not a valid server name"))?
367        .to_owned();
368    let handle = conn_handle(conn)?;
369    let mut guard = handle.lock().unwrap();
370    let plain = match &mut *guard {
371        Conn::Tls(_) => return Err(format!("tls: connection {conn} is already secured")),
372        Conn::Plain(s) => s.try_clone().map_err(|e| err_recv(&e))?,
373    };
374    let client = rustls::ClientConnection::new(cfg, server)
375        .map_err(|e| format!("tls: handshake could not start for '{host}': {e}"))?;
376    let mut stream = rustls::StreamOwned::new(client, plain);
377    // ★ HANDSHAKE NOW, NOT ON FIRST WRITE. A caller that gets `Ok` has a VERIFIED peer; deferring
378    // it would report success for a certificate that is about to be rejected, and the first
379    // `tcpSend` would fail with an error about sending.
380    stream
381        .conn
382        .complete_io(&mut stream.sock)
383        .map_err(|e| format!("tls: handshake with '{host}' failed: {e}"))?;
384    *guard = Conn::Tls(Box::new(stream));
385    Ok(())
386}
387
388/// Set the connection's read+write timeout in milliseconds (`0` clears it) —
389/// Python `settimeout` style: it applies to every subsequent recv/send, whose
390/// expiry is the shared `tcp: operation timed out` error.
391pub fn set_timeout(conn: i64, millis: i64) -> Result<(), String> {
392    if millis < 0 {
393        return Err(err_invalid_timeout(millis));
394    }
395    let dur = (millis > 0).then(|| std::time::Duration::from_millis(millis as u64));
396    let mut reg = registry().lock().unwrap();
397    if let Some(entry) = reg.conns.get(&conn) {
398        // Through the CONTROL handle, so arming a deadline on a connection that is already
399        // blocked in `read` works — that is the case a deadline exists for.
400        entry.ctl.set_read_timeout(dur).map_err(|e| err_recv(&e))?;
401        entry.ctl.set_write_timeout(dur).map_err(|e| err_send(&e))?;
402        return Ok(());
403    }
404    if reg.listeners.contains_key(&conn) {
405        match dur {
406            Some(d) => reg.listener_timeouts.insert(conn, d),
407            None => reg.listener_timeouts.remove(&conn),
408        };
409        return Ok(());
410    }
411    Err(err_unknown_conn(conn))
412}
413
414/// Send every byte (each Int masked to 0–255); `Ok` carries the byte count.
415/// `data` arrives as the AOT tier's borrowed `Seq of Int` parameter form.
416pub fn send(conn: i64, data: &[i64]) -> Result<i64, String> {
417    let bytes = seq_to_bytes(data);
418    let handle = conn_handle(conn)?;
419    let mut stream = handle.lock().unwrap();
420    stream.write_all(&bytes).map_err(|e| {
421        if is_timeout(&e) {
422            err_timeout()
423        } else {
424            err_send(&e)
425        }
426    })?;
427    Ok(bytes.len() as i64)
428}
429
430/// Block until at least one byte arrives (or EOF), returning up to `max_len`
431/// bytes. A clean EOF is `Ok` of the empty Seq — a value, not an error.
432pub fn recv(conn: i64, max_len: i64) -> Result<LogosSeq<i64>, String> {
433    if !(0..=MAX_RECV).contains(&max_len) {
434        return Err(err_invalid_len(max_len));
435    }
436    let handle = conn_handle(conn)?;
437    let mut stream = handle.lock().unwrap();
438    let mut buf = vec![0u8; max_len as usize];
439    let n = stream.read(&mut buf).map_err(|e| {
440        if is_timeout(&e) {
441            err_timeout()
442        } else {
443            err_recv(&e)
444        }
445    })?;
446    buf.truncate(n);
447    Ok(bytes_to_seq(buf))
448}
449
450/// Block until exactly `len` bytes arrive; a peer closing early is an `Err`
451/// (a torn frame is never silently truncated — protocol framing depends on it).
452pub fn recv_exact(conn: i64, len: i64) -> Result<LogosSeq<i64>, String> {
453    if !(0..=MAX_RECV).contains(&len) {
454        return Err(err_invalid_len(len));
455    }
456    let handle = conn_handle(conn)?;
457    let mut stream = handle.lock().unwrap();
458    let mut buf = vec![0u8; len as usize];
459    let mut got = 0usize;
460    while got < buf.len() {
461        match stream.read(&mut buf[got..]) {
462            Ok(0) => return Err(err_torn(got, len)),
463            Ok(n) => got += n,
464            Err(e) if is_timeout(&e) => return Err(err_timeout()),
465            Err(e) => return Err(err_recv(&e)),
466        }
467    }
468    Ok(bytes_to_seq(buf))
469}
470
471/// Close the connection or listener; the handle is dead afterwards.
472pub fn close(conn: i64) -> Result<(), String> {
473    let mut reg = registry().lock().unwrap();
474    reg.listener_timeouts.remove(&conn);
475    if let Some(entry) = reg.conns.remove(&conn) {
476        // ★ DROP, DO NOT SHUT DOWN — and this was a real regression, caught by
477        // `http_workers::a_handler_error_is_a_500_and_the_server_survives`. Forcing
478        // `shutdown(Both)` here looked like the way to make `close` wake a reader parked on the
479        // duplicated descriptor, but a socket shut down with unread data in its receive buffer is
480        // RESET rather than closed, so a client waiting on an already-written 500 got a connection
481        // reset instead of the response. Closing a connection is not the same act as cancelling
482        // one.
483        //
484        // Cancellation has its own verbs and they work while a read is parked, which is the point
485        // of the control descriptor: `tcpSetTimeout` arms a deadline and `tcpShutdownWrite` sends
486        // FIN, both without taking the io lock. `close` stays a graceful release.
487        drop(entry);
488        return Ok(());
489    }
490    if let Some(listener) = reg.listeners.remove(&conn) {
491        drop(listener);
492        return Ok(());
493    }
494    Err(err_unknown_conn(conn))
495}
496
497/// Bind a listening socket (passive open; port `0` = ephemeral).
498pub fn listen(port: i64) -> Result<i64, String> {
499    if !(0..=65535).contains(&port) {
500        return Err(err_invalid_port(port));
501    }
502    match std::net::TcpListener::bind(("0.0.0.0", port as u16)) {
503        Ok(listener) => {
504            let mut reg = registry().lock().unwrap();
505            let id = reg.next;
506            reg.next += 1;
507            reg.listeners.insert(id, listener);
508            Ok(id)
509        }
510        Err(e) => Err(err_listen(port, &e)),
511    }
512}
513
514/// Accept one queued connection (blocks; a `set_timeout` on the listener applies).
515pub fn accept(listener: i64) -> Result<i64, String> {
516    let deadline = {
517        let reg = registry().lock().unwrap();
518        if !reg.listeners.contains_key(&listener) {
519            return Err(err_unknown_conn(listener));
520        }
521        reg.listener_timeouts
522            .get(&listener)
523            .map(|d| std::time::Instant::now() + *d)
524    };
525    loop {
526        // Lock per poll so a long accept never wedges other connections.
527        let accepted = {
528            let mut reg = registry().lock().unwrap();
529            let l = reg
530                .listeners
531                .get_mut(&listener)
532                .ok_or_else(|| err_unknown_conn(listener))?;
533            if deadline.is_some() {
534                l.set_nonblocking(true).map_err(|e| err_accept(&e))?;
535            }
536            match l.accept() {
537                Ok((stream, _peer)) => {
538                    stream.set_nonblocking(false).map_err(|e| err_accept(&e))?;
539                    stream.set_nodelay(true).map_err(|e| err_accept(&e))?;
540                    let id = reg.next;
541                    match new_entry(stream) {
542                        Ok(entry) => {
543                            reg.next += 1;
544                            reg.conns.insert(id, entry);
545                            Some(Ok(id))
546                        }
547                        Err(e) => Some(Err(e)),
548                    }
549                }
550                Err(e) if is_timeout(&e) && deadline.is_some() => None,
551                Err(e) => Some(Err(err_accept(&e))),
552            }
553        };
554        match accepted {
555            Some(result) => return result,
556            None => {
557                if let Some(dl) = deadline {
558                    if std::time::Instant::now() >= dl {
559                        return Err(err_timeout());
560                    }
561                }
562                std::thread::sleep(std::time::Duration::from_millis(5));
563            }
564        }
565    }
566}
567
568/// The local port a listener is bound to (how an ephemeral bind is learned).
569pub fn listener_port(listener: i64) -> Result<i64, String> {
570    let reg = registry().lock().unwrap();
571    let l = reg
572        .listeners
573        .get(&listener)
574        .ok_or_else(|| err_unknown_conn(listener))?;
575    l.local_addr()
576        .map(|a| a.port() as i64)
577        .map_err(|e| err_accept(&e))
578}
579
580/// Half-close: send FIN on the write side (`shutdown(SHUT_WR)`), keep reading.
581pub fn shutdown_write(conn: i64) -> Result<(), String> {
582    let reg = registry().lock().unwrap();
583    let entry = reg.conns.get(&conn).ok_or_else(|| err_unknown_conn(conn))?;
584    entry
585        .ctl
586        .shutdown(std::net::Shutdown::Write)
587        .map_err(|e| err_shutdown(&e))
588}