Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
asio_local_stream_socket_fwd.hpp
Go to the documentation of this file.
1/* Flow-IPC: Core
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
22#include "ipc/util/util_fwd.hpp"
24#include <flow/log/log.hpp>
25#include <boost/asio.hpp>
26#include <ostream>
27
28/**
29 * Additional (versus boost.asio) APIs for advanced work with local (Unix domain) stream and connected-datagram
30 * sockets including transmission of native handles through them; and peer process credentials acquisition.
31 *
32 * @note Naming: "stream" in `asio_local_stream_socket` means *connected* local socket -- both
33 * #Protocol_byte_stream (`SOCK_STREAM`) and #Protocol_pkt_stream (`SOCK_SEQPACKET`) -- not the former only.
34 *
35 * ### Rationale ###
36 * These exist, in the first place, because internally such things as Native_socket_stream needed them for
37 * impl purposes. However they are of general usefulness publicly and hence are not tucked away under `detail/`.
38 * Because, from a public API point of view, they are orthogonal to the main public APIs (like Native_socket_stream),
39 * they are in a segregated namespace.
40 *
41 * That said, to conserve time without sacrificing reusability, generally speaking features were implemented only
42 * when there was an active use case for each -- or the cost of adding them was low. Essentially APIs are written
43 * in such a way as to be generally usable in the same spirit as built-in boost.asio APIs -- or at least reasonably
44 * natural to get to that point in the future.
45 *
46 * ### Overview ###
47 * As of this writing `asio_local_stream_socket` has the following features w/r/t local (Unix domain) sockets.
48 * - Convenience aliases (`local_ns`, #Peer_socket, #Acceptor, #Endpoint, etc.).
49 * - Socket option for use with boost.asio API `Peer_socket::get_option()` that gets the opposing process's
50 * credentials (PID, UID, ...) (Opt_peer_process_credentials).
51 * - Writing of blob + native handle combos (boost.asio supports only the former)
52 * (nb_write_some_with_native_handle()).
53 * - Reading of blob + native handle combos (boost.asio supports only the former)
54 * (nb_read_some_with_native_handle()).
55 * - Batch-reading of same (a-la Linux `recvmmsg()`). Msg_batch_in class template does not simply wrap a single
56 * receive-message-batch OS-call; rather it retains the data structures from read to read, enabling repeated
57 * batch-reading with ~no setup cost per batch-read.
58 *
59 * The socket-transmission APIs generally support, via template argument, two types of local (Unix domain) sockets,
60 * at least in Linux:
61 * - Streams (`SOCK_STREAM` type): By far the most commonly used type out there, this is the socket type that is
62 * TCP-like (but local, not networked). That is, connected socket + stream of bytes, no message boundaries.
63 * - In boost.asio this is known as `stream_protocol` (#Protocol_byte_stream).
64 * - Connected-datagram (`SOCK_SEQPACKET` type): A somewhat exotic socket type, it has these characteristics:
65 * - Connected socket, same as the stream-type. (So still TCP-like in that sense.)
66 * - Stream of *datagrams*, meaning message boundaries exist and are preserved. (Thus UDP-like in that sense.)
67 * - In boost.asio this is known as `seq_packet_protocol` (#Protocol_pkt_stream).
68 *
69 * By definition batch-transmission (as via Msg_batch_in) can only apply to a message-boundary-preserving protocol;
70 * therefore any support for batching applies only to #Protocol_pkt_stream, not #Protocol_byte_stream.
71 *
72 * @todo `asio_local_stream_socket` additional feature: APIs that can read and write native sockets together with
73 * accompanying binary blobs can be extended to handle an arbitrary number of native handles (per call) as opposed to
74 * only 0 or 1. The main difficulty here is designing a convenient and stylish, yet performant, API. Having achieved
75 * this we could also similarly extend the Native_handle_sender, Native_handle_receiver
76 * concepts and their impls.
77 *
78 * @todo Analogously to how Msg_batch_in supports batch-receipt, we could add API(s) for batch-send. Having achieved
79 * this we could also similarly extend the Blob_sender, Native_handle_sender concepts and their impls. Without getting
80 * into details: sending and receiving, especially in batches, are not really mirror images of each other conceptually.
81 * Hence lacking one but not the other is not necessarily an omission due to lack of time; the need for it is not as
82 * clear (we feel); and the API(s) would probably be designed fairly differently.
83 */
85{
86
87// Types.
88
89/* (The @namespace and @brief thingies shouldn't be needed, but some Doxygen bug necessitated them.
90 * See flow::log::fs for explanation... same thing here.) */
91
92/**
93 * @namespace ipc::transport::asio_local_stream_socket::local_ns
94 * @brief Short-hand for boost.asio Unix domain socket namespace. In particular `connect_pair()` free function lives
95 * here.
96 */
97namespace local_ns = boost::asio::local;
98
99/**
100 * Short-hand for boost.asio Unix domain stream-socket -- without built-in message boundary preservation -- protocol.
101 *
102 * In Unix-world this is known as `AF_LOCAL+SOCK_STREAM` (a/k/a `AF_UNIX+SOCK_STREAM`) and is widely supported in
103 * POSIX-land. It is similar to TCP (but local, with all simplifications this entails), and a key point is that
104 * it *does not preserve message boundaries*. That is, if I try to OS-write 10 bytes, it might write only the first
105 * 5 (however, if an ancillary native-handle/FD was part of the write, then the FD will have been written if and only if
106 * the OS-write reported writing-out at least 1 byte). Therefore to send a bounded message, one must use
107 * a length prefix or use a sentinel scheme.
108 */
109using Protocol_byte_stream = local_ns::stream_protocol;
110
111#ifndef FLOW_OS_LINUX
112static_assert(false, "Flow-IPC has some support for AF_LOCAL/SOCK_SEQPACKET and relies on Linux semantics for it; "
113 "this may be available in other OS with alleged spotty support, and we have not tested it "
114 "elsewhere. For now build in Linux only.");
115#endif
116
117/**
118 * Short-hand for boost.asio Unix domain stream-socket -- *with* built-in message boundary preservation -- protocol.
119 *
120 * In Linux-world this is known as `AF_LOCAL+SOCK_SEQPACKET` (a/k/a `AF_UNIX+SOCK_SEQPACKET`) and is known to be
121 * well supported in Linux after a certain vintage. (It is allegedly supported in some other Unixes and is allegedly
122 * mentioned but not required by POSIX. So far we have only tested in Linux, and generally internal code paths that
123 * use it in Flow-IPC can fall-back to #Protocol_byte_stream if so configured at compile-time.)
124 *
125 * It is identical to #Protocol_byte_stream, including in terms of endpoints, `connect()` behavior, and `connect_pair()`
126 * behavior. The one, key difference is it *does preserve message boundaries*. That is, if I try to OS-write 10 bytes,
127 * it will either write 0 bytes, or it will write 10 bytes; it will not write 5 (or 2, or 9). Accordingly:
128 * on the other end, each OS-read will -- regardless of the size of the supplied buffer (except that it is big enough
129 * to accept the message), or how many bytes are actually readable -- return either 0 bytes or N bytes, and N shall
130 * equal exactly the # of bytes written (and requested to be written) by an OS-write call on the other end.
131 *
132 * The handle/FD transmission semantics are likely as one would expect and arguably even simpler than with
133 * #Protocol_byte_stream; a handle/FD (if any) one attempts to be transmitted in an OS-write was indeed transmitted if
134 * and only if the message was (no worries about it "belonging to byte 1").
135 *
136 * ### Rationale ###
137 * Dealing in bounded messages, a/k/a datagrams, is pretty common (almost universal). So naturally this is simply
138 * convenient in many cases -- all else being equal; it is not necessary to send length-bearing prefixes or
139 * sentinels/escaping. Beyond convenience, though, when trying to squeeze out all possible performance from an
140 * IPC system it allows one to significantly reduce the number of I/O syscalls, thus loading the kernel less
141 * (reduced kernel locking).
142 *
143 * It also allows for the advanced feature, available at least in Linux, which is batched sends/receives
144 * via `sendmmsg()` and `recvmmsg()`. These guys allow one to send or receive several messages at once, with a
145 * single syscall. Msg_batch_in provides receive-batching support.
146 *
147 * ### Versus "datagram" protocol ###
148 * `AF_LOCAL+SOCK_DGRAM` is -- in terms of send/receive semantics -- in practice identical. (I (ygoldfel) should
149 * say in theory in practice; it is what various docs say; but we have not as of this writing heavily verified it.)
150 * It is also, subjectively speaking, less exotic/obscure -- perhaps owing to being similar to UDP (but
151 * reliable and non-reordering, in this local context).
152 *
153 * However it is connectionless. (Its `connect()` behavior is very different and is more of a memory and filter;
154 * there is no built-in graceful-close "token" semantic; and it cannot be generated via `connect_pair()`.) This is
155 * not necessarily worse; but it is different. Therefore as of this writing internal code paths in Flow-IPC
156 * use either #Protocol_byte_stream or #Protocol_pkt_stream.
157 */
158using Protocol_pkt_stream = local_ns::seq_packet_protocol;
159
160/**
161 * Short-hand for boost.asio Unix domain stream-socket acceptor (listening guy) socket.
162 * @tparam Protocol
163 * At least, `Protocol_byte_stream` or `Protocol_pkt_stream`.
164 */
165template<typename Protocol>
166using Acceptor = typename Protocol::acceptor;
167
168/**
169 * Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
170 * @tparam Protocol
171 * At least, `Protocol_byte_stream` or `Protocol_pkt_stream`.
172 */
173template<typename Protocol>
174using Peer_socket = typename Protocol::socket;
175
176/**
177 * Short-hand for boost.asio Unix domain peer stream-socket endpoint.
178 * @tparam Protocol
179 * At least, `Protocol_byte_stream` or `Protocol_pkt_stream`.
180 */
181template<typename Protocol>
182using Endpoint = typename Protocol::endpoint;
183
184// Find doc headers near the bodies of these compound types.
185
186template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
187class Msg_batch_in;
188
190
191// Free functions.
192
193/**
194 * boost.asio extension similar to
195 * `peer_socket->non_blocking(true); auto n = peer_socket->send(payload_blob)` with the added
196 * capability of accompanying the `Blob_const payload_blob` with a native handle to be transmitted to the
197 * opposing peer.
198 *
199 * In other words it attempts to immediately send `payload_hndl` and at least 1 byte of `payload_blob`
200 * (when `Protocol` is `Protocol_byte_stream`) or all of it (when `Protocol` is `Protocol_pkt_stream`);
201 * returns would-block error code if this would require blocking; or another error if the connection has become hosed.
202 * Performing `peer_socket->send()` given `peer_socket->non_blocking() == true` has the same semantics except
203 * it cannot and will not transmit any native handle.
204 *
205 * @see Please read the "Blob/handle semantics" about working with native
206 * handle accompaniment, in the nb_read_some_with_native_handle() doc header.
207 *
208 * Certain aspects of `Peer_socket::send()` are not included in the present function, however, though merely
209 * because they were not necessary as of this writing and hence excluded for simplicity; these are formally described
210 * below.
211 *
212 * ### Formal behavior ###
213 * This function requires that `payload_blob` be non-empty; and `payload_hndl.null() == false`.
214 * (If you want to send a non-empty buffer but no handle, then just use boost.asio `Peer_socket::send()`.
215 * As of this writing the relevant OS shall not support receiving a handle but a null buffer.)
216 *
217 * The function exits without blocking. The sending occurs synchronously, if possible, or does not occur otherwise.
218 * A successful operation is defined as sending 1+ bytes of the blob (for #Protocol_byte_stream) or
219 * all bytes of the blob (for #Protocol_pkt_stream); and the native handle. It is not possible
220 * that the native handle is transmitted but 0 bytes of the blob (or, for #Protocol_pkt_stream, not-all bytes of
221 * the blob) are. See `flow::Error_code` docs for error reporting
222 * semantics (if `err_code` is non-null, `*err_code` is set to code or success; else exception with that code is
223 * thrown in the former [non-success] case). (Informally: Most of the time, assuming no error condition on the
224 * connection, the function will return success. This deals with local (Unix domain as of this writing) peer
225 * connections; and the other side likely uses ipc::transport::Native_socket_stream which takes care to read incoming
226 * messages ASAP at all times; therefore would-block when sending should be rarer than even with remote TCP traffic.)
227 *
228 * The following are all the possible outcomes:
229 * - `N > 0` is returned (specifically `N` equals total size of `payload_blob` for #Protocol_pkt_stream);
230 * and `*err_code == {}` if non-null.
231 * This indicates 1 or more (`N`) bytes of the buffer, and the handle, were sent successfully.
232 * - If `N` is less than total size of `payload_blob`, then the remaining bytes cannot currently
233 * be sent without blocking and should be tried later.
234 * - This is possible with #Protocol_byte_stream but not #Protocol_pkt_stream.
235 * - If non-null `err_code`, then `N == 0` is returned; and `*err_code == E` is set to the triggering problem.
236 * If null, then `flow::error::Runtime_error` is thrown containing `Error_code E`.
237 * - `E == boost::asio::error::would_block` specifically indicates the non-fatal condition wherein `*peer_socket`
238 * cannot currently send, until it reaches writable state again.
239 * - Other `E` values indicate the connection is (potentially gracefully) permanently incapable of transmission.
240 * - `E == operation_aborted` is not possible.
241 *
242 * Items are extensively logged on `*logger_ptr`, and we follow the normal best practices to avoid verbose messages
243 * at the severities strictly higher than `TRACE`. In particular, any error is logged as a `WARNING`, so in particular
244 * there's no need for caller to specifically log about the details of a non-false `E`.
245 *
246 * ### Features of `Peer_socket::send()` not provided here ###
247 * We have (consciously) made these concessions:
248 * - This function never blocks, regardless of `peer_socket->non_blocking()`. `send()` -- if unable to
249 * immediately send 1+ bytes -- will block until it can, if `peer_socket->non_blocking() == false` mode had been
250 * set. (That's why we named it `nb_...()`.)
251 *
252 * The following is not a concession, and these words may be redundant, but: `Peer_socket::send()` has
253 * ~2 overloads; one that throws exception on error; and one that takes an `Error_code&`; whereas we combine the two
254 * via the `Error_code*` null-vs-not dichotomy. (It's redundant, because it's just following the Flow pattern.)
255 *
256 * Lastly, `Peer_socket::send()` has an overload wherein one can
257 * pass in a (largely unportable, I (ygoldfel) think) `message_flags` bit mask. We do not provide this feature: again
258 * because it is not needed, but also because depending on the flag it may lead to unexpected corner cases, and we'd
259 * rather not deal with those unless needed in practice.
260 *
261 * ### Rationale ###
262 * This function exists because elsewhere in ipc::transport needed it internally. It is a public API basically
263 * opportunistically: it's generic enough to be useful in its own right potentially, but as of this writing there's
264 * no use case. This explains the aforementioned concessions compared to boost.asio's function(s).
265 * All can be implemented without controversy. If we wanted to make an "official-looking" boost.asio extension then
266 * there would be merit in no longer conceding those concessions.
267 *
268 * @param logger_ptr
269 * Logger to use for subsequently logging.
270 * @param peer_socket
271 * Pointer to socket. If it is not connected, or otherwise unsuitable, behavior is identical to
272 * attempting `send()` on such a socket. If null behavior is undefined (assertion may trip).
273 * @param payload_hndl
274 * The native handle to transmit. If `payload_hndl.null()` behavior is undefined (possible
275 * assertion trip). Reiterating the above outcome semantics: if the return value `N` indicates even 1 byte
276 * was sent, then this was successfully sent also.
277 * @param payload_blob
278 * The buffer (possibly scattered) to transmit.
279 * Reiterating the above outcome semantics: Either there is no error, and then the
280 * `N` returned will be 1+; or a truthy `Error_code` is returned either via the out-arg or via thrown
281 * `Runtime_error`, and in the former case 0 is returned.
282 * @param err_code
283 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
284 * `boost::asio::error::would_block` (socket not writable, likely because other side isn't reading ASAP),
285 * other system codes (see notes above in the outcome discussion).
286 * @return 0 if non-null `err_code` and truthy resulting `*err_code`, and hence no bytes or the handle was sent; 1+
287 * if that number of bytes were sent plus the native handle (and hence falsy `*err_code` if non-null).
288 * For #Protocol_pkt_stream, if this returns not-zero, then it shall equal the total size in `payload_blob`.
289 * @tparam Protocol
290 * At least, `Protocol_byte_stream` or `Protocol_pkt_stream`.
291 * @tparam Const_buffer_sequence
292 * See `ConstBufferSequence` concept in boost.asio docs. For your convenience, refresher:
293 * This is often simply util::Blob_const (a single, non-scattered buffer); or
294 * `std::array<util::Blob_const, 2>` or `boost::array<util::Blob_const, 2>` (usually for a prefix-frame of
295 * known length and a payload frame of arbitrary length); or beyond that `vector` or `list` (etc.) of
296 * util::Blob_const. (To obtain a `Blob_const` there exist boost.asio adapters for many common
297 * single-buffer-storing/representing containers/types including `vector<uint8_t>` and such.)
298 */
299template<typename Protocol, typename Const_buffer_sequence>
300size_t nb_write_some_with_native_handle(flow::log::Logger* logger_ptr,
301 Peer_socket<Protocol>* peer_socket,
302 Native_handle payload_hndl,
303 const Const_buffer_sequence& payload_blob,
304 Error_code* err_code);
305
306/**
307 * boost.asio extension similar to
308 * `peer_socket->non_blocking(true); auto n = peer_socket->receive(target_payload_blob)` with the added
309 * capability of reading (from opposing peer) not only `target_payload_blob` but an optionally accompanying
310 * native handle.
311 *
312 * In other words it attempts to immediately read at least 1 byte into `target_payload_blob`
313 * and, if also present, the native handle into `*target_payload_hndl`; returns would-block error code if this
314 * would require blocking; or another error if the connection has become hosed. Performing `peer_socket->receive()`
315 * given `peer_socket->non_blocking() == true` has the same semantics except it cannot and will not read any native
316 * handle. (It would probably just "eat it"/ignore it; though we have not tested that at this time.)
317 *
318 * Informally speaking:
319 * - For #Protocol_byte_stream: Message boundaries are not respected (are not a thing). On successful read of
320 * 1+ bytes, the number of bytes received shall be as many as can fit into `target_payload_blob` and are
321 * pending in the kernel.
322 * - For #Protocol_pkt_stream: Message boundaries are respected. On successful read of
323 * 1+ bytes, the number of bytes received shall be as many as are
324 * contained in the *next pending datagram* of length equal to what was supplied in the corresponding
325 * write call on the opposing side of the connection. (If these cannot fit into `target_payload_blob`, it
326 * is an error; see below.)
327 *
328 * Certain aspects of `Peer_socket::receive()` are not included in the present function, however, though merely
329 * because they were not necessary as of this writing and hence excluded for simplicity; these are formally described
330 * below.
331 *
332 * ### Formal behavior ###
333 * This function requires that `target_payload_blob` be non-empty. It shall at entry set `*target_payload_hndl`
334 * so that `target_payload_hndl->null() == true`. `target_payload_hndl` (the pointer) must not be null.
335 *
336 * @note Suppose `Protocol` is #Protocol_byte_stream; what if you expect no handle? Then you have two choices.
337 * (1) If you would rather just ignore an incoming handle, even if it does arrive, then use boost.asio's
338 * `Peer_socket::receive()` (or `read_some()`). (2) If you want to catch that case (perhaps to throw an error),
339 * then use a `Native_handle dummy` together with this free function; and act accordingly if
340 * `dummy.null() == false` on return.
341 *
342 * @note Suppose `Protocol` is #Protocol_pkt_stream; what if you expect no handle? Then:
343 * Firstly, **do not simply use `Peer_socket::receive()`**! It does not, at least, handle the buffer-overflow
344 * (`target_payload_blob` too small) situation in a nice and portable way, whereas the present free function does,
345 * and you should take advantage of that. So: use a `Native_handle dummy` together with this free function;
346 * and either ignore its out-value (if you don't care whether they sent handle after all), or act appropriately
347 * if `dummy.null() == false` on return (if you do care).
348 *
349 * The function exits without blocking. The receiving occurs synchronously, if possible, or does not occur otherwise.
350 * A successful operation is defined as receiving 1+ bytes into the blob; and the native handle if it was present.
351 * It is not possible that a native handle is received but 0 bytes of the blob are. See `flow::Error_code` docs for
352 * error reporting semantics (if `err_code` is non-null, `*err_code` is set to code or success; else exception with
353 * that code is thrown in the former [non-success] case).
354 *
355 * The following are all the possible outcomes:
356 * - `N > 0` is returned; and `*err_code == {}` if non-null.
357 * This indicates 1 or more (`N`) bytes were placed at the start of the buffer, and *if* exactly 1 native handle
358 * handle was transmitted along with some subset of those `N` bytes, *then* it was successfully received into
359 * `*target_payload_hndl`; or else the fact there were exactly 0 such handles was successfully determined and
360 * reflected via `target_payload_hndl->null() == true`.
361 * - If `N` is less than the total size of `target_payload_blob` then:
362 * - (For #Protocol_byte_stream) no further bytes can currently be read without blocking and should
363 * be tried later if desired. (Informally: In that case consider `Peer_socket::async_wait()` followed by
364 * retrying the present function.)
365 * - (For #Protocol_pkt_stream) this is not particularly special, nor is it conceptually different from
366 * `N` being exactly equal to the total size of `target_payload_blob`.
367 * It just means the next pending in-datagram was of size `N`;
368 * it would be perfectly reasonable to try another read, as there may be more in-datagram(s) following.
369 * - If non-null `err_code`, then `N == 0` is returned; and `*err_code == E` is set to the triggering problem.
370 * If null, then `flow::error::Runtime_error` is thrown containing `Error_code E`.
371 * - `E == boost::asio::error::would_block` specifically indicates the non-fatal condition wherein `*peer_socket`
372 * cannot currently receive, until it reaches readable state again (i.e., bytes and possibly handle arrive from
373 * peer).
374 * - Other `E` values indicate a true error.
375 * - In most cases this means the connection is (potentially gracefully) permanently incapable of transmission.
376 * - In particular `E == boost::asio::error::eof` indicates the connection was gracefully closed by peer.
377 * (Informally, this is usually not to be treated differently from other fatal errors like
378 * `boost::asio::error::connection_reset`.)
379 * - The errors error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE and
380 * error::Code::S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL are the only ones which do *not*
381 * mean the socket is hosed.
382 * - `E == operation_aborted` is not possible.
383 *
384 * Items are extensively logged on `*logger_ptr`, and we follow the normal best practices to avoid verbose messages
385 * at the severities strictly higher than `TRACE`. In particular, any error is logged as a `WARNING`, so in particular
386 * there's no need for caller to specifically log about the details of a non-false `E`.
387 * (If this is still too slow, you may use the `flow::log::Config::this_thread_verbosity_override_auto()` to
388 * temporarily, in that thread only, disable logging. This is quite easy and performant.)
389 *
390 * ### Blob/handle semantics: `Protocol_byte_stream` ###
391 * Non-blocking stream-blob-send/receive semantics must be familiar to you, such as from TCP and otherwise.
392 * By adding native handles (further, just *handles*) as accompaniment to this system, non-trivial -- arguably
393 * subtle -- questions are raised about how it all works together. The central question is, perhaps, if I ask to send
394 * N bytes and handle S, non-blockingly, what are the various possibilities for sending less than N bytes of the blob
395 * and whether S is also sent? Conversely, how will receiving on the other side work? The following describes those
396 * semantics and *mandates* how to properly handle it. Not following these formally leads to undefined behavior.
397 * (Informally, for the tested OS versions, it is possible to count on certain additional behaviors; but trying to do
398 * so is (1) prone to spurious changes and breakage in different OS versions and types, since much of this is
399 * undocumented; and (2) will probably just make your life *more* difficult anyway, not less. Honestly I (ygoldfel)
400 * designed it for ease of following as opposed to exactly maximal freedom of capability. So... just follow these.)
401 *
402 * Firstly, as already stated, it is not possible to try sending a handle sans a blob; and it is not possible
403 * to receive a handle sans a blob. (The converse is a regular non-blocking blob write or receive op.)
404 *
405 * Secondly, one must think of the handle as associated with *exactly the first byte* of the blob arg to the
406 * write call (nb_write_some_with_native_handle()). Similarly, one must think of the handle as associated with
407 * *exactly the first byte* of the blob arg to the read call (nb_read_some_with_native_handle()). Moreover,
408 * known OS make certain not-well-documented assumptions about message lengths. What does this all
409 * mean in practice?
410 * - You may design your protocol however you want, except the following requirement: Define a
411 * *handle-containing message* as a combination of a blob of 1+ bytes of some known (on both sides, at the time
412 * of both sending and receipt) length N *and* exactly *one* handle. You must aim to send this message and
413 * receive it exactly as sent, meaning with message boundaries respected. (To be clear, you're free to use
414 * any technique to make N known on both sides; e.g., it may be a constant; or it may be passed in a previous
415 * message. However, it's not compatible with using a sentinel alone, as then N is unknown.)
416 * - You must only transmit handles as part of handle-containing messages. Anything else is undefined behavior.
417 * - Let M be a given handle-containing message with blob B of size N; and handle H.
418 * Let a *write op* be nb_write_some_with_native_handle().
419 * - You shall attempt one write op for the blob B of size N together with handle H. Do *not* intermix it with any
420 * other bytes or handles.
421 * - In the non-blocking write op case (nb_write_some_with_native_handle()) it may yield successfully sending
422 * N' bytes, where 1 <= N' < N. This means the handle was successfully sent also, because the handle is
423 * associated with the *first byte* of the write -- and read -- op. If this happens, don't worry about it;
424 * continue with the rest of the protocol, including sending at least the remaining (N - N') bytes of M.
425 * - On the receiver side, you must symmetrically execute the read op (nb_read_some_with_native_handle(), perhaps
426 * after a `Peer_socket::async_wait()`) to attempt receipt of all of M, including supplying a target
427 * blob of exactly N bytes -- without mixing with any other bytes or handles. The "hard" part of this is mainly
428 * to avoid having the previous read op "cut into" M.
429 * - (Informally, a common-sense way to do it just make
430 * your protocol message-based, such that the length of the next message is always known on either side.)
431 * - Again, if the nb_read_some_with_native_handle() call returns N', where 1 <= N' < N, then no worries.
432 * The handle H *will* have been successfully received, being associated with byte 1 of M.
433 * Keep reading the rest of M (namely, the remaining (N - N') bytes of the blob B) with more read op(s).
434 * - (To put a fine point on it: In known Linux versions as of this writing, if you do try to read-op N' bytes
435 * having executed write-op with N'' bytes, where N'' > N', then you may observe very strange, undefined
436 * (albeit non-crashy), behavior such as H disappearing or replacing a following-message handle H'. Don't.)
437 *
438 * That is admittedly many words, but really in practice it's fairly natural and simple to design a message-based
439 * protocol and implementation around it. Just do follow these; I merely wanted to be complete.
440 *
441 * ### Blob/handle semantics: `Protocol_pkt_stream` ###
442 * The situation is quite a bit simpler than for `Protocol_byte_stream`. A single write-call will send a datagram
443 * of N bytes, and either a native handle or none; and the corresponding read-call will receive that datagram
444 * (same contents including length) and the same handle, or none.
445 *
446 * However: If the total size of `target_payload_blob` is less than `M`, where `M` is the size of the next pending
447 * in-datagram in the relevant kernel buffer, then:
448 * - We emit error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE, and the in-dgram is lost. A warning may be logged,
449 * possibly indicating how long the in-dgram would have been.
450 * - The connection is *not* hosed: further I/O may be attempted.
451 * - (Informally: The practical implication of this is: It's best to agree on a max-size, so that you can
452 * supply a target-buffer that is always of that size and hence sufficiently large to handle any in-datagram.)
453 *
454 * @note Internally, in Linux, we do this using a combination of the `MSG_TRUNC` in-flag and the incidentally
455 * eponymous out-flag. Do not supply `message_flags = MSG_TRUNC`; that stuff is our responsibility.
456 *
457 * ### Features of `Peer_socket::receive()` not provided here ###
458 * We have (consciously) made these concessions: ...see nb_write_some_with_native_handle() doc header. All of the
459 * listed omitted features have common-sense counterparts in the case of the present function, except that we do
460 * provide a `message_flags` arg.
461 *
462 * ### Rationale ###
463 * This function exists because... [text omitted -- same reasoning as similar rationale for
464 * nb_write_some_with_native_handle()].
465 *
466 * @param logger_ptr
467 * Logger to use for subsequently logging.
468 * @param peer_socket
469 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
470 * attempting `receive()` on such a socket. If null behavior is undefined (assertion may trip).
471 * @param target_payload_hndl
472 * The native handle wrapper into which to copy the received handle; it shall be set such that
473 * `target_payload_hndl->null() == true` if the read-op returned 1+ (succeeded), but those bytes were not
474 * accompanied by any native handle. It shall also be thus set if 0 is returned (indicating error
475 * including would-block and fatal errors).
476 * @param target_payload_blob
477 * The buffer (possibly scattered) into which to write received blob data, namely up to the total size
478 * of this (possibly) scattered buffer. If the total size is 0, behavior is undefined (assertion may trip).
479 * Reiterating the above outcome semantics: Either there is no error, and then the `N` returned will be 1+; or a
480 * truthy `Error_code` is returned either via the out-arg or via thrown `Runtime_error`, and in the former case
481 * 0 is returned.
482 * @param err_code
483 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
484 * `boost::asio::error::would_block` (socket not readable: no data pending),
485 * ipc::transport::error::Code::S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL
486 * (strictly more than 1 handle detected in the read-op, but we support only 1 at this time; see above;
487 * maybe they didn't use above write-op function(s) and/or didn't follow anti-straddling suggestion above),
488 * ipc::transport::error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE
489 * (#Protocol_pkt_stream only: next pending in-datagram exceeds total size of `target_payload_blob`;
490 * the in-dgram is lost, but the connection is *not* hosed; see above),
491 * other system codes (see notes above in the outcome discussion).
492 * @param message_flags
493 * See boost.asio `Peer_socket::receive()` overload with this arg. As of this writing we see utility for
494 * `MSG_CMSG_CLOEXEC` (Linux at least); but otherwise have not delved into the effects of other flags.
495 * Also do not pass-in `MSG_DONTWAIT` or `MSG_TRUNC` (formally, behavior undefined).
496 * @return 0 if non-null `err_code` and truthy resulting `*err_code`, and hence no bytes nor a handle was received; 1+
497 * if that number of bytes were received plus either a native handle or none (and hence falsy `*err_code`
498 * if non-null). For #Protocol_pkt_stream: we reiterate that non-0 means that the in-datagram
499 * was sized N (where N is return value here).
500 *
501 * @tparam Protocol
502 * At least, `Protocol_byte_stream` or `Protocol_pkt_stream`.
503 * @tparam Mutable_buffer_sequence
504 * See `MutableBufferSequence` concept in boost.asio docs. See `Const_buffer_sequence` param doc header
505 * for nb_write_some_with_native_handle(); a similar refresher applies here but as applied to
506 * util::Blob_mutable instead of util::Blob_const.
507 *
508 * @internal
509 * ### Implementation notes -- `Protocol_byte_stream` ###
510 * Where does the content of "Blob/handle semantics" originate? Answer: Good question, as reading `man` pages to do
511 * with `sendmsg()/recvmsg()/cmsg/unix`, etc., gives hints but really is incomplete and certainly not formally complete.
512 * Without such a description, one can guess at how `SOL_SOCKET/SCM_RIGHTS` (sending of FDs along with blobs) might work
513 * but not conclusively. I (ygoldfel) nevertheless actually correctly developed the relevant conclusions via common
514 * sense/experience... and *later* confirmed them by reading kernel source and the delightfully helpful
515 * write-up at [ https://gist.github.com/kentonv/bc7592af98c68ba2738f4436920868dc ] (Googled "SCM_RIGHTS gist").
516 * Reading these may give the code inspector/maintainer (you?) more peace of mind. Basically, though, the key gist
517 * is:
518 * - The handle(s) are associated with byte 1 of the blob given to the `sendmsg()` call containing those handle(s).
519 * For this reason, to avoid protocol chaos, you should send each given handle with the same "synchronized" byte
520 * on both sides.
521 * - The length of that blob similarly matters -- which is not normal, as otherwise message boundaries are *not*
522 * normally maintained for stream connections -- and for this reason the read op must accept a result into a blob
523 * of at *least* the same size as the corresponding write op. (For simplicity and other reasons my
524 * instructions say it should just be equal.)
525 *
526 * ### Implementation notes -- `Protocol_pkt_stream` ###
527 * This (instead of `Protocol_byte_stream`) removes all that ambiguity. On the other hand it's not super-well
528 * documented itself (in `man` pages and such, that is).
529 */
530template<typename Protocol, typename Mutable_buffer_sequence>
531size_t nb_read_some_with_native_handle(flow::log::Logger* logger_ptr,
532 Peer_socket<Protocol>* peer_socket,
533 Native_handle* target_payload_hndl,
534 const Mutable_buffer_sequence& target_payload_blob,
535 Error_code* err_code,
536 int message_flags = 0);
537
538/**
539 * Serializes a Msg_batch_in to a standard output stream.
540 *
541 * @relatesalso Msg_batch_in
542 *
543 * @param os
544 * Stream to which to serialize.
545 * @param val
546 * Value to serialize.
547 * @return `os`.
548 */
549template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
550std::ostream& operator<<(std::ostream& os,
552
553} // namespace ipc::transport::asio_local_stream_socket
Similarly to the transport::Msg_batch_in concept (as taken by API {Blob|Native_handle}_receiver::asyn...
Gettable (read-only) socket option for use with asio_local_stream_socket::Peer_socket ....
Additional (versus boost.asio) APIs for advanced work with local (Unix domain) stream and connected-d...
typename Protocol::socket Peer_socket
Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
size_t nb_read_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket< Protocol > *peer_socket_ptr, Native_handle *target_payload_hndl_ptr, const Mutable_buffer_sequence &target_payload_blob, Error_code *err_code, int message_flags)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->receive(target...
std::ostream & operator<<(std::ostream &os, const Msg_batch_in< Mutable_buffer_sequence_t, Msg_resource_t > &val)
Serializes a Msg_batch_in to a standard output stream.
local_ns::seq_packet_protocol Protocol_pkt_stream
Short-hand for boost.asio Unix domain stream-socket – with built-in message boundary preservation – p...
typename Protocol::endpoint Endpoint
Short-hand for boost.asio Unix domain peer stream-socket endpoint.
local_ns::stream_protocol Protocol_byte_stream
Short-hand for boost.asio Unix domain stream-socket – without built-in message boundary preservation ...
typename Protocol::acceptor Acceptor
Short-hand for boost.asio Unix domain stream-socket acceptor (listening guy) socket.
size_t nb_write_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket< Protocol > *peer_socket_ptr, Native_handle payload_hndl, const Const_buffer_sequence &payload_blob, Error_code *err_code)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->send(payload_b...
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.