morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
tcp_socket.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <arpa/inet.h>
5#include <fcntl.h>
6#include <netdb.h>
7#include <poll.h>
8#include <sys/socket.h>
9#include <unistd.h>
10
11#include <algorithm>
12#include <cerrno>
13#include <chrono>
14#include <cstddef>
15#include <cstdint>
16#include <limits>
17#include <optional>
18#include <stdexcept>
19#include <string>
20#include <system_error>
21#include <utility>
22
23#if defined(__APPLE__)
24#include <signal.h>
25#endif
26
27#ifndef MSG_NOSIGNAL
28// macOS has no MSG_NOSIGNAL; SO_NOSIGPIPE (set per-socket below) is the
29// portable equivalent there, so this becomes a harmless no-op flag.
30#define MSG_NOSIGNAL 0
31#endif
32
33namespace morph::net::detail {
34
35/// @brief How a `pollUntil()` wait ended.
36enum class PollOutcome : std::uint8_t {
37 kReady, ///< `poll()` reported the descriptor (readiness, hang-up or error bits alike).
38 kTimedOut, ///< The deadline passed first.
39 kFailed, ///< `poll()` failed with something other than `EINTR`; see `PollResult::error`.
40};
41
42/// @brief The result of a `pollUntil()` wait.
43struct PollResult {
44 /// @brief How the wait ended.
45 PollOutcome outcome = PollOutcome::kTimedOut;
46 /// @brief The `errno` of the failing `poll()` when `outcome` is `kFailed`; `0` otherwise.
47 int error = 0;
48};
49
50/// @brief Waits for @p events on @p fd until @p deadline, with one budget for the whole wait.
51///
52/// A signal that interrupts `poll()` (`EINTR`) does not end the wait and does
53/// not re-arm the full timeout: the loop recomputes what is left of the budget
54/// and polls again, so a host delivering signals steadily (a profiler's timer,
55/// `SIGCHLD`) cannot stretch the wait past @p deadline. The per-call timeout is
56/// clamped to `int` milliseconds, which is what `poll()` takes; an unclamped
57/// multi-week budget would truncate to garbage, possibly negative, meaning
58/// "wait forever".
59///
60/// Reports rather than throws, so each caller keeps its own policy for a
61/// timeout or a failure (a connect moves on to the next address candidate; a
62/// handshake read gives up).
63///
64/// @param descriptor Descriptor to wait on.
65/// @param events `poll()` event mask, e.g. `POLLIN` or `POLLOUT`.
66/// @param deadline Point after which the wait reports `kTimedOut`. A deadline
67/// already passed reports it without polling.
68/// @return The outcome, plus the failing `errno` for `kFailed`.
69// A descriptor and an event mask are different things that happen to convert; the
70// parameter names carry the distinction.
71// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
72inline PollResult pollUntil(int descriptor, short events, std::chrono::steady_clock::time_point deadline) {
73 pollfd pfd{};
74 pfd.fd = descriptor;
75 pfd.events = events;
76 for (;;) {
77 // Rounded up: poll() takes whole milliseconds, and truncating would wake
78 // it up to a millisecond before the deadline and report a timeout early.
79 auto const remaining =
80 std::chrono::ceil<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now());
81 if (remaining.count() <= 0) {
82 return {.outcome = PollOutcome::kTimedOut};
83 }
84 auto const waitMs = static_cast<int>(
85 std::min<std::chrono::milliseconds::rep>(remaining.count(), std::numeric_limits<int>::max()));
86 int const ready = ::poll(&pfd, 1, waitMs);
87 if (ready > 0) {
88 return {.outcome = PollOutcome::kReady};
89 }
90 if (ready < 0) {
91 int const err = errno;
92 if (err != EINTR) {
93 return {.outcome = PollOutcome::kFailed, .error = err};
94 }
95 }
96 // EINTR, or a poll that returned early with nothing ready (the
97 // millisecond rounding above): recompute what is left and go again.
98 }
99}
100
101/// @brief RAII wrapper around a POSIX (BSD sockets) TCP file descriptor.
102///
103/// Linux/macOS only today — see `docs/spec/core/backend.md`'s `morph::net`
104/// section for the Windows/Winsock2 follow-up. Move-only.
105class TcpSocket {
106public:
107 /// @brief Constructs an empty (invalid) socket.
108 TcpSocket() = default;
109
110 /// @brief Wraps an already-open file descriptor (e.g. from `::accept`).
111 ///
112 /// Normalises the two fd properties this class's API depends on: SIGPIPE
113 /// suppression, and **blocking mode**. A `TcpSocket` is blocking unless its
114 /// owner asks otherwise with `setNonBlocking()`, because `recvSome()` and
115 /// `sendAll()` are written for blocking descriptors — neither treats
116 /// `EAGAIN` as anything but a fatal error.
117 ///
118 /// The blocking reset is not hypothetical bookkeeping.
119 /// macOS/BSD propagate a listening socket's `O_NONBLOCK` onto the sockets
120 /// `accept(2)` returns; POSIX permits that and Linux documents that it does
121 /// not do it. Without this, every connection `tryAccept()` hands back from
122 /// a non-blocking listener on macOS/BSD would be non-blocking too, and a
123 /// blocking reader such as `performServerHandshake()` would throw on
124 /// `EAGAIN` before the client's Upgrade request had arrived. Clearing it
125 /// here rather than in `tryAccept()` covers `accept()`, `connect()` and
126 /// `listen()` by the same rule, and leaves one place where the mode is
127 /// decided.
128 ///
129 /// Best effort, like `applyNoSigPipe`: an fd whose flags cannot be read or
130 /// written fails its first read anyway, which every caller already handles.
131 /// @param rawFd Native socket file descriptor; takes ownership.
132 explicit TcpSocket(int rawFd) : _fd{rawFd} {
133 applyNoSigPipe(rawFd);
134 clearNonBlocking(rawFd);
135 }
136
137 TcpSocket(const TcpSocket&) = delete;
138 TcpSocket& operator=(const TcpSocket&) = delete;
139
140 /// @brief Transfers ownership of @p other's file descriptor.
141 /// @param other Socket to move from; left invalid.
142 TcpSocket(TcpSocket&& other) noexcept : _fd{other._fd} { other._fd = -1; }
143
144 /// @brief Transfers ownership of @p other's file descriptor, closing this one first.
145 /// @param other Socket to move from; left invalid.
146 /// @return `*this`.
147 TcpSocket& operator=(TcpSocket&& other) noexcept {
148 if (this != &other) {
149 closeNow();
150 _fd = other._fd;
151 other._fd = -1;
152 }
153 return *this;
154 }
155
156 /// @brief Closes the socket if open.
157 ~TcpSocket() { closeNow(); }
158
159 /// @brief Connects to `host:port`, failing after @p timeout.
160 /// @param host Numeric or resolvable hostname.
161 /// @param port TCP port.
162 /// @param timeout Maximum time to wait for the connection to establish,
163 /// across *all* resolved addresses -- one deadline for the
164 /// whole call, not one per candidate.
165 /// @return A connected `TcpSocket`.
166 /// @throws std::runtime_error on resolution failure, connect failure, or timeout.
167 static TcpSocket connect(const std::string& host, std::uint16_t port, std::chrono::milliseconds timeout) {
168 addrinfo hints{};
169 hints.ai_family = AF_UNSPEC;
170 hints.ai_socktype = SOCK_STREAM;
171 addrinfo* resolved = nullptr;
172 // Widened explicitly: std::to_string has no uint16_t overload, and
173 // letting the integral promotion pick one trips GCC's -Wsign-promo
174 // (it would choose the signed `int` overload over `unsigned`).
175 std::string const portStr = std::to_string(static_cast<unsigned>(port));
176 int const rc = ::getaddrinfo(host.c_str(), portStr.c_str(), &hints, &resolved);
177 if (rc != 0 || resolved == nullptr) {
178 // `::gai_strerror`, and NOT `errnoMessage()`: `rc` is an `EAI_*`
179 // code, not an `errno`, so `std::system_category().message(rc)`
180 // would render a confidently wrong string.
181 //
182 // It also stays, rather than being replaced the way `std::strerror`
183 // was, because it does not have `std::strerror`'s thread-safety
184 // defect. This throw site can run on several threads at once (see
185 // `errnoMessage` below), so the question is live; what follows is
186 // measured rather than assumed.
187 //
188 // glibc 2.44, `gcc -O0`: `gai_strerror` returns a pointer to a
189 // string literal inside libc's own read-only data, distinct per
190 // code and stable across calls --
191 //
192 // EAI_AGAIN ptr=0x7fa71e9b62f2 "Temporary failure in name resolution"
193 // EAI_FAIL ptr=0x7fa71e9b632e "Non-recoverable failure in name resolution"
194 // dladdr -> /usr/lib/libc.so.6, anonymous (a literal in a data section)
195 // 7fa71e99f000-7fa71ea15000 r--p ... /usr/lib/libc.so.6
196 //
197 // The `r--p` mapping is the load-bearing part: a shared scratch
198 // buffer would have to be writable. Even an unrecognised code
199 // returns a constant ("Unknown error"), not a formatted one. The
200 // musl/emscripten implementation morph's WASM build uses is a
201 // `static const char msgs[]` table by inspection, same property.
202 // `TcpSocket` is POSIX-only (this file's own class comment), so
203 // Winsock's documented-unsafe `gai_strerrorA` never applies.
204 //
205 // Re-check if a platform with a different libc joins CI.
206 throw std::runtime_error("TcpSocket::connect: getaddrinfo failed for " + host + ": " + ::gai_strerror(rc));
207 }
208 struct AddrInfoGuard {
209 addrinfo* p;
210 ~AddrInfoGuard() { ::freeaddrinfo(p); }
211 } guard{resolved};
212
213 // One deadline for the whole call, not one timeout per candidate.
214 // `ai_family = AF_UNSPEC` makes several candidates the norm ("localhost"
215 // resolves to both ::1 and 127.0.0.1), and polling `timeout` inside the
216 // loop would make the worst case N x timeout -- while two doc comments
217 // (here and SocketBackend's destructor note) state it as a single
218 // bound. A deadline computed once keeps the stated bound true.
219 auto const deadline = std::chrono::steady_clock::now() + timeout;
220
221 for (addrinfo* rp = resolved; rp != nullptr; rp = rp->ai_next) {
222 int fd = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
223 if (fd < 0) {
224 continue;
225 }
226 int const flags = ::fcntl(fd, F_GETFL, 0);
227 ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
228 int const connectRc = ::connect(fd, rp->ai_addr, rp->ai_addrlen);
229 if (connectRc == 0) {
230 ::fcntl(fd, F_SETFL, flags);
231 return TcpSocket{fd};
232 }
233 if (errno != EINPROGRESS) {
234 ::close(fd);
235 continue;
236 }
237 // A timeout or a poll failure on this candidate moves on to the
238 // next; only running out of candidates is an error. A delivered
239 // signal is neither (pollUntil retries across EINTR).
240 if (pollUntil(fd, POLLOUT, deadline).outcome != PollOutcome::kReady) {
241 ::close(fd);
242 continue;
243 }
244 int soErr = 0;
245 socklen_t soErrLen = sizeof(soErr);
246 // Checked: a failing getsockopt leaves `soErr` at 0, which would
247 // otherwise hand back a broken socket as successfully connected.
248 if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soErr, &soErrLen) != 0 || soErr != 0) {
249 ::close(fd);
250 continue;
251 }
252 ::fcntl(fd, F_SETFL, flags);
253 return TcpSocket{fd};
254 }
255 throw std::runtime_error("TcpSocket::connect: could not connect to " + host + ":" + portStr);
256 }
257
258 /// @brief Creates a listening socket bound to `127.0.0.1:port`.
259 /// @param port TCP port to bind to; `0` lets the OS choose a free port.
260 /// @param backlog Pending-connection backlog passed to `::listen`.
261 /// @return A listening `TcpSocket`.
262 /// @throws std::runtime_error on socket/bind/listen failure.
263 static TcpSocket listen(std::uint16_t port, int backlog = 64) {
264 int fd = ::socket(AF_INET, SOCK_STREAM, 0);
265 if (fd < 0) {
266 throw std::runtime_error("TcpSocket::listen: socket() failed: " + errnoMessage(errno));
267 }
268 int const reuse = 1;
269 ::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
270 sockaddr_in addr{};
271 addr.sin_family = AF_INET;
272 addr.sin_port = htons(port);
273 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
274 if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
275 int const err = errno;
276 ::close(fd);
277 throw std::runtime_error("TcpSocket::listen: bind() failed: " + errnoMessage(err));
278 }
279 if (::listen(fd, backlog) != 0) {
280 int const err = errno;
281 ::close(fd);
282 throw std::runtime_error("TcpSocket::listen: listen() failed: " + errnoMessage(err));
283 }
284 return TcpSocket{fd};
285 }
286
287 /// @brief Returns the local port this socket is bound to, or `0` if invalid.
288 /// @return The bound TCP port (useful after `listen(0, ...)`).
289 [[nodiscard]] std::uint16_t boundPort() const {
290 if (_fd < 0) {
291 return 0;
292 }
293 sockaddr_in addr{};
294 socklen_t len = sizeof(addr);
295 ::getsockname(_fd, reinterpret_cast<sockaddr*>(&addr), &len);
296 return ntohs(addr.sin_port);
297 }
298
299 /// @brief Blocks until a client connects, returning the accepted connection.
300 ///
301 /// **This call has no portable interruption mechanism.** `shutdown(2)` on a
302 /// *listening* socket unblocks a parked `accept()` on Linux, but that is a
303 /// Linux property rather than a POSIX one — on macOS/BSD the parked thread
304 /// stays parked. Anything that has to be able to stop waiting
305 /// must therefore not park here at all: use `setNonBlocking()` plus a
306 /// readiness wait, and take the connection with `tryAccept()` — or hand
307 /// the listener to an event loop, as `SocketServer` does. This blocking
308 /// overload remains for callers that wait for exactly one known-imminent
309 /// connection (the socket-level tests).
310 /// @return The accepted `TcpSocket`.
311 /// @throws std::runtime_error if `::accept` fails.
312 TcpSocket accept() {
313 for (;;) {
314 int const clientFd = ::accept(_fd, nullptr, nullptr);
315 if (clientFd >= 0) {
316 return TcpSocket{clientFd};
317 }
318 // EINTR is not a failure, just a signal delivered while blocked, and
319 // `recvSome`/`sendAll` already retry it. Without the same retry
320 // here, any signal the host happens to deliver (a profiler's timer,
321 // SIGCHLD, SIGWINCH) tears down the accept loop and the server
322 // silently stops taking connections.
323 //
324 // Every other errno throws, and no other one is retried.
325 // ECONNABORTED in particular is not special-cased: `shutdown(
326 // listenfd, SHUT_RDWR)` unblocks a parked `accept()` with EINVAL,
327 // not ECONNABORTED, and shutting the listener down is not how
328 // anything here breaks out of an accept loop -- `SocketServer`
329 // polls a self-pipe instead and never parks here at all.
330 //
331 // No ECONNABORTED retry is added in its place. A reset-race repro
332 // over six shapes (blocking and non-blocking + poll, with and
333 // without TCP_DEFER_ACCEPT, with and without data, 1- and 8-deep
334 // queues) produced zero ECONNABORTED across 8000 `accept()` calls
335 // on Linux: the kernel queues the reset connection, `accept`
336 // succeeds, and the abort surfaces on the next `recv` as
337 // ECONNRESET -- which `recvSome` already absorbs as an orderly
338 // close. A retry loop here would therefore be behaviour no test on
339 // this project's only CI platform could make fail.
340 if (errno == EINTR) {
341 continue;
342 }
343 throw std::runtime_error("TcpSocket::accept: " + errnoMessage(errno));
344 }
345 }
346
347 /// @brief Puts this socket into non-blocking mode (`O_NONBLOCK`).
348 ///
349 /// Applied by `SocketServer` to its listening socket: `poll()` reporting a
350 /// listener readable does not guarantee the following `accept()` will not
351 /// block (a peer that resets in between takes the pending connection away),
352 /// and a listener that parks again after the wakeup has been consumed would
353 /// never be woken a second time.
354 /// @return `true` if the flag was applied; `false` on an empty socket or an
355 /// `fcntl` failure.
356 // Non-const for the same reason shutdownBoth() is: it mutates the open file
357 // description this object owns, and a const overload would advertise a call
358 // that changes nothing. ::fcntl is variadic by POSIX's design, as every
359 // other fcntl call in this file already is.
360 // NOLINTNEXTLINE(readability-make-member-function-const)
361 [[nodiscard]] bool setNonBlocking() noexcept {
362 if (_fd < 0) {
363 return false;
364 }
365 int const flags = ::fcntl(_fd, F_GETFL, 0);
366 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
367 return flags >= 0 && ::fcntl(_fd, F_SETFL, flags | O_NONBLOCK) == 0;
368 }
369
370 /// @brief Non-blocking counterpart of `accept()`, for a listener that
371 /// `setNonBlocking()` has been applied to.
372 ///
373 /// The connection it yields is **blocking**, whatever mode this listener is
374 /// in: the fd-adopting constructor clears `O_NONBLOCK`, which is what stops
375 /// macOS/BSD's inheritance of the listener's flag reaching `recvSome()`
376 /// Any rewrite of this function has to keep going through that
377 /// constructor, or do the reset itself.
378 /// @return The accepted `TcpSocket`, or `std::nullopt` when no connection
379 /// was pending — a readiness report that went stale before the
380 /// `accept`, which the caller answers by waiting again.
381 /// @throws std::runtime_error if `::accept` fails for any other reason.
382 // Non-const to match accept(): taking a connection consumes it from the
383 // listener's queue, which is state this object owns.
384 // NOLINTNEXTLINE(readability-make-member-function-const)
385 std::optional<TcpSocket> tryAccept() {
386 for (;;) {
387 int const clientFd = ::accept(_fd, nullptr, nullptr);
388 if (clientFd >= 0) {
389 return TcpSocket{clientFd};
390 }
391 int const err = errno;
392 if (err == EINTR) { // retried for the same reason accept() retries it
393 continue;
394 }
395 if (wouldBlock(err)) {
396 return std::nullopt;
397 }
398 throw std::runtime_error("TcpSocket::tryAccept: " + errnoMessage(err));
399 }
400 }
401
402 /// @brief Reads up to @p len bytes into @p buf.
403 /// @param buf Destination buffer.
404 /// @param len Capacity of @p buf.
405 /// @return Number of bytes read; `0` means the peer closed the connection
406 /// (or a concurrent `shutdownBoth()` unblocked this call).
407 /// @throws std::runtime_error on a socket error other than an internally
408 /// retried `EINTR` or a peer reset (treated as an orderly close).
409 std::size_t recvSome(char* buf, std::size_t len) {
410 for (;;) {
411 ssize_t const n = ::recv(_fd, buf, len, 0);
412 if (n < 0) {
413 if (errno == EINTR) {
414 continue;
415 }
416 if (errno == ECONNRESET) {
417 return 0;
418 }
419 throw std::runtime_error("TcpSocket::recvSome: " + errnoMessage(errno));
420 }
421 return static_cast<std::size_t>(n);
422 }
423 }
424
425 /// @brief Writes all @p len bytes of @p data, looping over partial writes.
426 /// @param data Bytes to send.
427 /// @param len Number of bytes in @p data.
428 /// @throws std::runtime_error on a socket error.
429 void sendAll(const char* data, std::size_t len) {
430 std::size_t sent = 0;
431 while (sent < len) {
432 ssize_t const n = ::send(_fd, data + sent, len - sent, MSG_NOSIGNAL);
433 if (n < 0) {
434 if (errno == EINTR) {
435 continue;
436 }
437 throw std::runtime_error("TcpSocket::sendAll: " + errnoMessage(errno));
438 }
439 sent += static_cast<std::size_t>(n);
440 }
441 }
442
443 /// @brief Bounds how long a single `::send` inside `sendAll()` may block.
444 ///
445 /// Without this a `sendAll` against a peer that has stopped reading blocks
446 /// forever once the kernel send buffer fills, and it does so while holding
447 /// whatever lock its caller took. With `SO_SNDTIMEO` set, the blocked
448 /// `send` returns `EAGAIN`/`EWOULDBLOCK` instead, `sendAll` throws as it
449 /// already does for any other send error, and the lock is released.
450 ///
451 /// A timeout is not a "slow link" cutoff: it bounds one `send` syscall that
452 /// is making *no* progress, so it should be set generously. Zero disables it
453 /// (the kernel default, block forever).
454 ///
455 /// @param timeout Per-`send` bound; zero to disable.
456 /// @return `true` if the option was applied.
457 [[nodiscard]] bool setSendTimeout(std::chrono::milliseconds timeout) const noexcept {
458 // Assigned without casts on purpose: `milliseconds::rep` and
459 // `timeval`'s members are both `long` on the platforms this builds for,
460 // so an explicit cast is an identity cast and GCC rejects it under
461 // -Werror=useless-cast. clang-tidy also wants names of three characters
462 // or more, hence `timeoutVal` rather than the conventional `tv`.
463 timeval timeoutVal{};
464 timeoutVal.tv_sec = timeout.count() / 1000;
465 timeoutVal.tv_usec = (timeout.count() % 1000) * 1000;
466 return ::setsockopt(_fd, SOL_SOCKET, SO_SNDTIMEO, &timeoutVal, sizeof(timeoutVal)) == 0;
467 }
468
469 /// @brief Bounds how long a single `::recv` (inside `recvSome()`) may block.
470 ///
471 /// Without this, a peer that completes the TCP handshake but never writes
472 /// a byte (e.g. a load balancer connecting lazily to its backend) parks
473 /// the caller in `recvSome` forever -- for `SocketBackend`'s client
474 /// handshake read specifically, that in turn wedges `~SocketBackend`,
475 /// since the destructor's escape hatch only reaches a socket already
476 /// published to `_socket`.
477 ///
478 /// A timed-out `recv` makes `recvSome` throw (`EAGAIN`/`EWOULDBLOCK` is
479 /// not one of the errors it treats as an orderly close), so callers meant
480 /// to block indefinitely during normal operation must disable this again
481 /// (`timeout` of zero) once whatever bounded phase needed it is over.
482 ///
483 /// @param timeout Per-`recv` bound; zero to disable.
484 /// @return `true` if the option was applied.
485 [[nodiscard]] bool setRecvTimeout(std::chrono::milliseconds timeout) const noexcept {
486 timeval timeoutVal{};
487 timeoutVal.tv_sec = timeout.count() / 1000;
488 timeoutVal.tv_usec = (timeout.count() % 1000) * 1000;
489 return ::setsockopt(_fd, SOL_SOCKET, SO_RCVTIMEO, &timeoutVal, sizeof(timeoutVal)) == 0;
490 }
491
492 /// @brief Shuts down both directions of the socket, unblocking a concurrent
493 /// `recvSome`/`sendAll` on another thread. Safe to call from any thread.
494 ///
495 /// Documented for *connected* sockets only, which is where `shutdown(2)`
496 /// waking a parked peer call is portable. It is not a way to interrupt
497 /// `accept()` on a listening socket — see `accept()`.
498 void shutdownBoth() noexcept {
499 if (_fd >= 0) {
500 ::shutdown(_fd, SHUT_RDWR);
501 }
502 }
503
504 /// @brief Returns the native file descriptor (`-1` if empty/moved-from).
505 /// @return The native fd.
506 [[nodiscard]] int nativeHandle() const noexcept { return _fd; }
507
508 /// @brief Gives up ownership of the descriptor without closing it.
509 ///
510 /// For handing a bound listener to something that adopts it, such as
511 /// `core::net::adoptListener`, which closes it from then on.
512 /// @return The descriptor (`-1` if empty); this socket is empty afterwards.
513 [[nodiscard]] int release() noexcept { return std::exchange(_fd, -1); }
514
515 /// @brief Returns whether this socket owns an open file descriptor.
516 /// @return `true` if `nativeHandle() >= 0`.
517 [[nodiscard]] bool valid() const noexcept { return _fd >= 0; }
518
519private:
520 /// Describes `err` the way `std::strerror` would, without `std::strerror`'s
521 /// shared static buffer.
522 ///
523 /// Every throw site in this class formats its message on whichever thread
524 /// hit the error, and sockets are driven from several threads at once — a
525 /// server loop and its clients' threads, in the tests. Two of them can
526 /// therefore be inside a throw site at the same moment, and `std::strerror`
527 /// is permitted to return a pointer to one buffer shared by all callers --
528 /// a data race on the message, not merely an interleaved string.
529 ///
530 /// `std::error_category::message` is specified with no such carve-out, so
531 /// it carries the library's ordinary "shall not introduce a data race"
532 /// guarantee, and it returns an owned `std::string`, leaving nothing for
533 /// two threads to share. `std::system_category()` is the category whose
534 /// values are `errno` values on POSIX, which is what every caller here
535 /// passes. Preferred over `strerror_r` because that function's XSI and GNU
536 /// variants differ in return type, so a portable call needs a build-time
537 /// discriminator and a caller-supplied buffer; this needs neither.
538 static std::string errnoMessage(int err) { return std::system_category().message(err); }
539
540 /// POSIX allows `EAGAIN` and `EWOULDBLOCK` to differ, and both name the
541 /// same "nothing to take right now" answer. Written as two statements
542 /// rather than `err == EAGAIN || err == EWOULDBLOCK` so GCC's
543 /// `-Wlogical-op` cannot read it as a disjunction of equal expressions on
544 /// the platforms (Linux, macOS) where the two macros expand to one value.
545 static bool wouldBlock(int err) noexcept {
546 if (err == EAGAIN) {
547 return true;
548 }
549 return err == EWOULDBLOCK;
550 }
551
552 /// Takes `rawFd` out of non-blocking mode, if it was in it. See the
553 /// fd-adopting constructor for why every adopted descriptor gets this.
554 static void clearNonBlocking(int rawFd) noexcept {
555 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) — ::fcntl is variadic by POSIX's design
556 int const flags = ::fcntl(rawFd, F_GETFL, 0);
557 if (flags < 0 || (flags & O_NONBLOCK) == 0) {
558 return;
559 }
560 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
561 static_cast<void>(::fcntl(rawFd, F_SETFL, flags & ~O_NONBLOCK));
562 }
563
564 static void applyNoSigPipe(int fd) {
565#if defined(SO_NOSIGPIPE)
566 int const one = 1;
567 ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
568#else
569 (void)fd;
570#endif
571 }
572
573 void closeNow() noexcept {
574 if (_fd >= 0) {
575 ::close(_fd);
576 _fd = -1;
577 }
578 }
579
580 int _fd{-1};
581};
582
583} // namespace morph::net::detail
Command host(std::string const &name, AnyHandle const &args={})
host: a command the application registered with the client.
Definition author.hpp:2870
Resolved const & resolved()
The process registries, matched and validated on the first call.
Definition effects.hpp:511