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}