Flow-IPC 1.0.1
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
27/**
28 * Additional (versus boost.asio) APIs for advanced work with local stream (Unix domain) sockets including
29 * transmission of native handles through such streams; and peer process credentials acquisition.
30 *
31 * ### Rationale ###
32 * These exist, in the first place, because internally such things as Native_socket_stream needed them for
33 * impl purposes. However they are of general usefulness publicly and hence are not tucked away under `detail/`.
34 * Because, from a public API point of view, they are orthogonal to the main public APIs (like Native_socket_stream),
35 * they are in a segregated namespace.
36 *
37 * That said, to conserve time without sacrificing reusability, generally speaking features were implemented only
38 * when there was an active use case for each -- or the cost of adding them was low. Essentially APIs are written
39 * in such a way as to be generally usable in the same spirit as built-in boost.asio APIs -- or at least reasonably
40 * natural to get to that point in the future.
41 *
42 * ### Overview ###
43 * As of this writing `asio_local_stream_socket` has the following features w/r/t local stream (Unix domain) sockets:
44 * - Convenience aliases (`local_ns`, #Peer_socket, #Acceptor, #Endpoint, etc.).
45 * - Socket option for use with boost.asio API `Peer_socket::get_option()` that gets the opposing process's
46 * credentials (PID, UID, ...) (Opt_peer_process_credentials).
47 * - Writing of blob + native handle combos (boost.asio supports only the former)
48 * (nb_write_some_with_native_handle(), async_write_with_native_handle(), etc.).
49 * - Reading of blob + native handle combos (boost.asio supports only the former)
50 * (nb_read_some_with_native_handle()).
51 * - More advanced composed blob reading operations (async_read_with_target_func(), at least).
52 *
53 * @todo At least asio_local_stream_socket::async_read_with_target_func() can be extended to
54 * other stream sockets (TCP, etc.). In that case it should be moved to a different namespace however
55 * (perhaps named `asio_stream_socket`; could then move the existing `asio_local_stream_socket` inside that one
56 * and rename it `local`).
57 *
58 * @todo `asio_local_stream_socket` additional feature: APIs that can read and write native sockets together with
59 * accompanying binary blobs can be extended to handle an arbitrary number of native handles (per call) as opposed to
60 * only 0 or 1. The main difficulty here is designing a convenient and stylish, yet performant, API.
61 *
62 * @todo `asio_local_stream_socket` additional feature: APIs that can read and write native handles together with
63 * accompanying binary blobs can be extended to handle scatter/gather semantics for the aforementioned blobs,
64 * matching standard boost.asio API functionality.
65 *
66 * @todo `asio_local_stream_socket` additional feature: Non-blocking APIs like nb_read_some_with_native_handle()
67 * and nb_write_some_with_native_handle() can gain blocking counterparts, matching standard boost.asio API
68 * functionality.
69 *
70 * @todo `asio_local_stream_socket` additional feature: `async_read_some_with_native_handle()` --
71 * async version of existing nb_read_some_with_native_handle(). Or another way to put it is,
72 * equivalent of boost.asio `Peer_socket::async_read_some()` but able to read native handle(s) with the blob.
73 * Note: This API would potentially be usable inside the impl of existing APIs (code reuse).
74 *
75 * @todo `asio_local_stream_socket` additional feature: `async_read_with_native_handle()` --
76 * async version of existing nb_read_some_with_native_handle(), plus the "stubborn" behavior of built-in `async_read()`
77 * free function. Or another way to put it is, equivalent of boost.asio `async_read<Peer_socket>()` but able to read
78 * native handle(s) with the blob.
79 * Note: This API would potentially be usable inside the impl of existing APIs (code reuse).
80 *
81 * @internal
82 * ### Implementation notes ###
83 * As one would expect the native-handle-transmission APIs use Linux `recvmsg()` and `sendmsg()` with the
84 * `SCM_RIGHTS` ancillary-message type. For some of the
85 * to-dos mentioned above:
86 * - Both of those functions take buffers in terms of `iovec` arrays; the present impl merely provides a 1-array.
87 * It would be pretty easy to extend this to do scatter/gather.
88 * - To offer blocking versions, one can simply start a `flow::async::Single_thread_task_loop` each time
89 * and `post()` onto it in `S_ASYNC_AND_AWAIT_CONCURRENT_COMPLETION` mode. Alternatively one could write more
90 * performant versions that would directly use the provided sockets in blocking mode; this would be much more work.
91 */
93{
94
95// Types.
96
97/* (The @namespace and @brief thingies shouldn't be needed, but some Doxygen bug necessitated them.
98 * See flow::util::bind_ns for explanation... same thing here.) */
99
100/**
101 * @namespace ipc::transport::asio_local_stream_socket::local_ns
102 * @brief Short-hand for boost.asio Unix domain socket namespace. In particular `connect_pair()` free function lives
103 * here.
104 */
105namespace local_ns = boost::asio::local;
106
107/// Short-hand for boost.asio Unix domain stream-socket protocol.
108using Protocol = local_ns::stream_protocol;
109
110/// Short-hand for boost.asio Unix domain stream-socket acceptor (listening guy) socket.
111using Acceptor = Protocol::acceptor;
112
113/// Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
114using Peer_socket = Protocol::socket;
115
116/// Short-hand for boost.asio Unix domain peer stream-socket endpoint.
117using Endpoint = Protocol::endpoint;
118
120
121// Free functions.
122
123/**
124 * boost.asio extension similar to
125 * `boost::asio::async_write(Peer_socket&, Blob_const, Task_err_sz)` with the added capability of
126 * accompanying the `Blob_const` with a native handle to be transmitted to the opposing peer.
127 *
128 * @see Please read the "Blob/handle semantics" about working with native handle
129 * handle accompaniment, in the nb_read_some_with_native_handle() doc header.
130 *
131 * boost.asio's `async_write()` free function is generically capable of sending a sequence of 1+ buffers on
132 * a connected stream socket, continuing until either the entire sequence is fully sent; or an error (not counting
133 * would-block, which just means keep trying to make progress when possible). The capability we add is the native
134 * handle in `payload_hndl` is also transmitted. Certain aspects of `async_write()` are not included in the present
135 * function, however, though merely because they were not necessary as of this writing and hence excluded for
136 * simplicity; these are formally described below.
137 *
138 * ### Formal behavior ###
139 * This function requires that `payload_blob` be non-empty; and `payload_hndl.null() == false`.
140 * (If you want to send a non-empty buffer but no handle, then just use boost.asio `async_write()`.
141 * As of this writing the relevant OS shall not support sending a handle but a null buffer.)
142 *
143 * The function exits without blocking. The sending occurs asynchronously. A successful operation is defined as
144 * sending all of the blob; and the native handle. `on_sent_or_error()` shall be called
145 * at most once, indicating the outcome of the operation. (Informally: Most of the time, though asynchronous, this
146 * should be a very fast op. This deals with local (Unix domain as of this writing) peer connections; and the other
147 * side likely uses ipc::transport::Native_socket_stream which takes care to read incoming messages ASAP at all times;
148 * therefore blocking when sending should be rarer than even with remote TCP traffic.) The following are all the
149 * possible outcomes:
150 * - `on_sent_or_error(Error_code())` is executed as if `post()`ed
151 * on `peer_socket->get_executor()` (the `flow::util::Task_engine`,
152 * a/k/a boost.asio `io_context`, associated with `*peer_socket`), where `N == payload_blob.size()`.
153 * This indicates the entire buffer, and the handle, were sent successfully.
154 * - `on_sent_or_error(E)`, where `bool(E) == true`, is executed similarly.
155 * This indicates the send did not fully succeed, and `E` specifies why this happened.
156 * No indication is given how many bytes were successfully sent (if any even were).
157 * (Informally, there's likely not much difference between those 2 outcomes. Either way, the connection is
158 * hosed.)
159 * - `E == boost::asio::error::operation_aborted` is possible and indicates your own code canceled pending
160 * async work such as by destroying `*peer_socket`. Informally, the best way to deal
161 * with it is know it's normal when stuff is shutting down; and to do nothing other than maybe logging,
162 * but even then to not assume all relevant objects even exist; really it's best to just return right away.
163 * Know that upon that return the handler's captured state will be freed, as in all cases.
164 * - `E` will never indicate would-block.
165 * - `on_sent_or_error()` is canceled and not called at all, such as possibly when `stop()`ing the underlying
166 * `Task_engine`. This is similar to the aforementioned `operation_aborted` situation.
167 * Know that upon that return the handler's captured state *will* be freed at the time of
168 * whatever shutdown/cancellation step.
169 *
170 * Items are extensively logged on `*logger_ptr`, and we follow the normal best practices to avoid verbose messages
171 * at the severities strictly higher than `TRACE`. In particular, any error is logged as a `WARNING`, so in particular
172 * there's no need to for caller to specifically log about the details of a non-false `Error_code`.
173 *
174 * Thread safety is identical to that of `async_write_some()`.
175 *
176 * ### Features of `boost::asio::async_write()` not provided here ###
177 * We have (consciously) made these concessions:
178 * - `payload_blob` is a single blob. `async_write()` is templated in such a way as to accept that or a
179 * *sequence* of `Blob_const`s, meaning it supports scatter/gather.
180 * - There are also fancier advanced-async-flow-control overloads of `async_write()` with more features we haven't
181 * replicated. However the simplest overload only has the preceding 3 bullet points on us.
182 *
183 * ### Rationale ###
184 * This function exists because elsewhere in ipc::transport needed it internally. It is a public API basically
185 * opportunistically: it's generic enough to be useful in its own right potentially, but as of this writing there's
186 * no use case. This explains the aforementioned concessions compared to boost.asio `async_write()` free function.
187 * All, without exception, can be implemented without controversy. It would be busy-work and was omitted
188 * simply because there was no need. If we wanted to make an "official-looking" boost.asio extension then there would
189 * be merit in no longer conceding those concessions.
190 *
191 * @internal
192 * Update: transport::Native_socket_stream's impl has been split into itself on top and
193 * transport::sync_io::Native_socket_stream as its core -- also available for public use. Because the latter
194 * is now the part doing the low-level I/O, by `sync_io` pattern's nature it can no longer be doing boost.asio
195 * async-waits but rather outsources them to the user of that object (transport::Native_socket_stream being
196 * a prime example). So that means the present function is no longer used by Flow-IPC internally as of this
197 * writing. Still it remains a perfectly decent API; so leaving it available.
198 *
199 * There are to-dos elsewhere to perhaps generalize this guy and his bro(s) to support both boost.asio
200 * and `sync_io`. It would be some ungodly API, but it could have a boost.asio-target wrapper unchanged from
201 * the current signature.
202 *
203 * These 3 paragraphs apply, to one extent or another, to async_write_with_native_handle(),
204 * async_read_with_target_func(), and async_read_interruptible().
205 * @endinternal
206 *
207 * @tparam Task_err
208 * boost.asio handler with same signature as `flow::async::Task_asio_err`.
209 * It can be bound to an executor (commonly, a `strand`); this will be respected.
210 * @param logger_ptr
211 * Logger to use for subsequently logging.
212 * @param peer_socket
213 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
214 * attempting `async_write()` on such a socket. If null behavior is undefined (assertion may trip).
215 * @param payload_hndl
216 * The native handle to transmit. If `payload_hndl.is_null()` behavior is undefined (possible
217 * assertion trip).
218 * @param payload_blob
219 * The buffer to transmit. Reiterating the above outcome semantics: Either there is no error, and then
220 * the `N` passed to the handler callback will equal `payload_blob.size()`; or there is a truthy `Error_code`,
221 * and `N` will be strictly less than `payload_blob.size()`.
222 * @param on_sent_or_error
223 * Handler to execute at most once on completion of the async op. It is executed as if via
224 * `post(peer_socket->get_executor())`, fully respecting any executor
225 * bound to it (via `bind_executor()`, such as a strand).
226 * It shall be passed `Error_code`. The semantics of these values are shown above.
227 * Informally: falsy `Error_code` indicates total success of sending both items; truthy `Error_code`
228 * indicates a connection-fatal error prevented us from some or both being fully sent; one should disregard
229 * `operation_aborted` and do nothing; else one should consider the connection as hosed (possibly gracefully) and
230 * take steps having discovered this.
231 */
232template<typename Task_err>
233void async_write_with_native_handle(flow::log::Logger* logger_ptr,
234 Peer_socket* peer_socket,
235 Native_handle payload_hndl, const util::Blob_const& payload_blob,
236 Task_err&& on_sent_or_error);
237
238/**
239 * boost.asio extension similar to
240 * `peer_socket->non_blocking(true); auto n = peer_socket->write_some(payload_blob)` with the added
241 * capability of accompanying the `Blob_const payload_blob` with a native handle to be transmitted to the
242 * opposing peer.
243 *
244 * In other words it attempts to immediately send `payload_hndl` and at least 1 byte of `payload_blob`;
245 * returns would-block error code if this would require blocking; or another error if the connection has become hosed.
246 * Performing `peer_socket->write_some()` given `peer_socket->non_blocking() == true` has the same semantics except
247 * it cannot and will not transmit any native handle.
248 *
249 * @see Please read the "Blob/handle semantics" about working with native
250 * handle accompaniment, in the nb_read_some_with_native_handle() doc header.
251 *
252 * Certain aspects of `Peer_socket::write_some()` are not included in the present function, however, though merely
253 * because they were not necessary as of this writing and hence excluded for simplicity; these are formally described
254 * below.
255 *
256 * ### Formal behavior ###
257 * This function requires that `payload_blob` be non-empty; and `payload_hndl.null() == false`.
258 * (If you want to send a non-empty buffer but no handle, then just use boost.asio `Peer_socket::write_some()`.
259 * As of this writing the relevant OS shall not support receive a handle but a null buffer.)
260 *
261 * The function exits without blocking. The sending occurs synchronously, if possible, or does not occur otherwise.
262 * A successful operation is defined as sending 1+ bytes of the blob; and the native handle. It is not possible
263 * that the native handle is transmitted but 0 bytes of the blob are. See `flow::Error_code` docs for error reporting
264 * semantics (if `err_code` is non-null, `*err_code` is set to code or success; else exception with that code is
265 * thrown in the former [non-success] case). (Informally: Most of the time, assuming no error condition on the
266 * connection, the function will return success. This deals with local (Unix domain as of this writing) peer
267 * connections; and the other side likely uses ipc::transport::Native_socket_stream which takes care to read incoming
268 * messages ASAP at all times; therefore would-block when sending should be rarer than even with remote TCP traffic.)
269 *
270 * The following are all the possible outcomes:
271 * - `N > 0` is returned; and `*err_code == Error_code()` if non-null.
272 * This indicates 1 or more (`N`) bytes of the buffer, and the handle, were sent successfully.
273 * If `N != payload_blob.size()`, then the remaining bytes cannot currently be sent without blocking and should
274 * be tried later. (Informally: Consider async_write_with_native_handle() in that case.)
275 * - If non-null `err_code`, then `N == 0` is returned; and `*err_code == E` is set to the triggering problem.
276 * If null, then `flow::error::Runtime_error` is thrown containing `Error_code E`.
277 * - `E == boost::asio::error::would_block` specifically indicates the non-fatal condition wherein `*peer_socket`
278 * cannot currently send, until it reaches writable state again.
279 * - Other `E` values indicate the connection is (potentially gracefully) permanently incapable of transmission.
280 * - `E == operation_aborted` is not possible.
281 *
282 * Items are extensively logged on `*logger_ptr`, and we follow the normal best practices to avoid verbose messages
283 * at the severities strictly higher than `TRACE`. In particular, any error is logged as a `WARNING`, so in particular
284 * there's no need to for caller to specifically log about the details of a non-false `E`.
285 *
286 * ### Features of `Peer_socket::write_some()` not provided here ###
287 * We have (consciously) made these concessions:
288 * - `payload_blob` is a single blob. `write_some()` is templated in such a way as to accept that or a
289 * *sequence* of `Blob_const`s, meaning it supports scatter/gather.
290 * - This function never blocks, regardless of `peer_socket->non_blocking()`. `write_some()` -- if unable to
291 * immediately send 1+ bytes -- will block until it can, if `peer_socket->non_blocking() == false` mode had been
292 * set. (That's why we named it `nb_...()`.)
293 *
294 * The following is not a concession, and these words may be redundant, but: `Peer_socket::write_some()` has
295 * 2 overloads; one that throws exception on error; and one that takes an `Error_code&`; whereas we combine the two
296 * via the `Error_code*` null-vs-not dichotomy. (It's redundant, because it's just following the Flow pattern.)
297 *
298 * Lastly, `Peer_socket::send()` is identical to `Peer_socket::write_some()` but adds an overload wherein one can
299 * pass in a (largely unportable, I (ygoldfel) think) `message_flags` bit mask. We do not provide this feature: again
300 * because it is not needed, but also because depending on the flag it may lead to unexpected corner cases, and we'd
301 * rather not deal with those unless needed in practice.
302 *
303 * ### Rationale ###
304 * This function exists because... [text omitted -- same reasoning as similar rationale for
305 * async_write_with_native_handle()].
306 *
307 * @param logger_ptr
308 * Logger to use for subsequently logging.
309 * @param peer_socket
310 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
311 * attempting `write_some()` on such a socket. If null behavior is undefined (assertion may trip).
312 * @param payload_hndl
313 * The native handle to transmit. If `payload_hndl.is_null()` behavior is undefined (possible
314 * assertion trip). Reiterating the above outcome semantics: if the return value `N` indicates even 1 byte
315 * was sent, then this was successfully sent also.
316 * @param payload_blob
317 * The buffer to transmit. Reiterating the above outcome semantics: Either there is no error, and then the
318 * `N` returned will be 1+; or a truthy `Error_code` is returned either via the out-arg or via thrown
319 * `Runtime_error`, and in the former case 0 is returned.
320 * @param err_code
321 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
322 * `boost::asio::error::would_block` (socket not writable, likely because other side isn't reading ASAP),
323 * other system codes (see notes above in the outcome discussion).
324 * @return 0 if non-null `err_code` and truthy resulting `*err_code`, and hence no bytes or the handle was sent; 1+
325 * if that number of bytes were sent plus the native handle (and hence falsy `*err_code` if non-null).
326 */
327size_t nb_write_some_with_native_handle(flow::log::Logger* logger_ptr,
328 Peer_socket* peer_socket,
329 Native_handle payload_hndl, const util::Blob_const& payload_blob,
330 Error_code* err_code);
331
332/**
333 * boost.asio extension similar to
334 * `peer_socket->non_blocking(true); auto n = peer_socket->read_some(target_payload_blob)` with the added
335 * capability of reading (from opposing peer) not only `Blob_mutable target_payload_blob` but an optionally accompanying
336 * native handle.
337 *
338 * In other words it attempts to immediately read at least 1 byte into `*target_payload_blob`
339 * and, if also present, the native handle into `*target_payload_hndl`; returns would-block error code if this
340 * would require blocking; or another error if the connection has become hosed. Performing `peer_socket->read_some()`
341 * given `peer_socket->non_blocking() == true` has the same semantics except it cannot and will not read any native
342 * handle. (It would probably just "eat it"/ignore it; though we have not tested that at this time.)
343 *
344 * Certain aspects of `Peer_socket::read_some()` are not included in the present function, however, though merely
345 * because they were not necessary as of this writing and hence excluded for simplicity; these are formally described
346 * below.
347 *
348 * ### Formal behavior ###
349 * This function requires that `target_payload_blob` be non-empty. It shall at entry set `*target_payload_hndl`
350 * so that `target_payload_hndl->null() == true`. `target_payload_hndl` (the pointer) must not be null.
351 * (If you want to receive into non-empty buffer but expect no handle, then just use boost.asio
352 * `Peer_socket::read_some()`. If you want to receive a non-empty buffer but no handle, then just use
353 * boost.asio `async_write()`. As of this writing the relevant OS shall not support receiving a handle but a
354 * null buffer.)
355 *
356 * The function exits without blocking. The receiving occurs synchronously, if possible, or does not occur otherwise.
357 * A successful operation is defined as receiving 1+ bytes into the blob; and the native handle if it was present.
358 * It is not possible that a native handle is received but 0 bytes of the blob are. See `flow::Error_code` docs for
359 * error reporting semantics (if `err_code` is non-null, `*err_code` is set to code or success; else exception with
360 * that code is thrown in the former [non-success] case).
361 *
362 * The following are all the possible outcomes:
363 * - `N > 0` is returned; and `*err_code == Error_code()` if non-null.
364 * This indicates 1 or more (`N`) bytes were placed at the start of the buffer, and *if* exactly 1 native handle
365 * handle was transmitted along with some subset of those `N` bytes, *then* it was successfully received into
366 * `*target_payload_hndl`; or else the fact there were exactly 0 such handles was successfully determined and
367 * reflected via `target_payload_hndl->null() == true`.
368 * If `N != target_payload_blob.size()`, then no further bytes can currently be read without blocking and should
369 * be tried later if desired. (Informally: In that case consider `Peer_socket::async_wait()` followed by retrying
370 * the present function, in that case.)
371 * - If non-null `err_code`, then `N == 0` is returned; and `*err_code == E` is set to the triggering problem.
372 * If null, then `flow::error::Runtime_error` is thrown containing `Error_code E`.
373 * - `E == boost::asio::error::would_block` specifically indicates the non-fatal condition wherein `*peer_socket`
374 * cannot currently receive, until it reaches readable state again (i.e., bytes and possibly handle arrive from
375 * peer).
376 * - Other `E` values indicate the connection is (potentially gracefully) permanently incapable of transmission.
377 * - In particular `E == boost::asio::error::eof` indicates the connection was gracefully closed by peer.
378 * (Informally, this is usually not to be treated differently from other fatal errors like
379 * `boost::asio::error::connection_reset`.)
380 * - It may be tempting to distinguish between "true" system errors (probably from `boost::asio::error::`)
381 * and "protocol" errors from `ipc::transport::error::Code` (as of this writing
382 * `S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL`): one *can* technically keep reading in the latter
383 * case, in that the underlying connection is still connected potentially. However, formally, behavior is
384 * undefined if one reads more subsequently. Informally: if the other side has violated protocol
385 * expectations -- or if your side has violated expectations on proper reading (see below section on that) --
386 * then neither side can be trusted to recover logical consistency and must abandon the connection.
387 * - `E == operation_aborted` is not possible.
388 *
389 * Items are extensively logged on `*logger_ptr`, and we follow the normal best practices to avoid verbose messages
390 * at the severities strictly higher than `TRACE`. In particular, any error is logged as a `WARNING`, so in particular
391 * there's no need for caller to specifically log about the details of a non-false `E`.
392 * (If this is still too slow, you may use the `flow::log::Config::this_thread_verbosity_override_auto()` to
393 * temporarily, in that thread only, disable logging. This is quite easy and performant.)
394 *
395 * ### Blob/handle semantics ###
396 * Non-blocking stream-blob-send/receive semantics must be familiar to you, such as from TCP and otherwise.
397 * By adding native handles (further, just *handles*) as accompaniment to this system, non-trivial -- arguably
398 * subtle -- questions are raised about how it all works together. The central question is, perhaps, if I ask to send
399 * N bytes and handle S, non-blockingly, what are the various possibilities for sending less than N bytes of the blob
400 * and whether S is also sent? Conversely, how will receiving on the other side work? The following describes those
401 * semantics and *mandates* how to properly handle it. Not following these formally leads to undefined behavior.
402 * (Informally, for the tested OS versions, it is possible to count on certain additional behaviors; but trying to do
403 * so is (1) prone to spurious changes and breakage in different OS versions and types, since much of this is
404 * undocumented; and (2) will probably just make your life *more* difficult anyway, not less. Honestly I (ygoldfel)
405 * designed it for ease of following as opposed to exactly maximal freedom of capability. So... just follow these.)
406 *
407 * Firstly, as already stated, it is not possible to try sending a handle sans a blob; and it is not possible
408 * to receive a handle sans a blob. (The converse is a regular non-blocking blob write or receive op.)
409 *
410 * Secondly, one must think of the handle as associated with *exactly the first byte* of the blob arg to the
411 * write call (nb_write_some_with_native_handle()). Similarly, one must think of the handle as associated with
412 * *exactly the first byte* of the blob arg to the read call (nb_read_some_with_native_handle()). Moreover,
413 * known OS make certain not-well-documented assumptions about message lengths. What does this all
414 * mean in practice?
415 * - You may design your protocol however you want, except the following requirement: Define a
416 * *handle-containing message* as a combination of a blob of 1+ bytes of some known (on both sides, at the time
417 * of both sending and receipt) length N *and* exactly *one* handle. You must aim to send this message and
418 * receive it exactly as sent, meaning with message boundaries respected. (To be clear, you're free to use
419 * any technique to make N known on both sides; e.g., it may be a constant; or it may be passed in a previous
420 * message. However, it's not compatible with using a sentinel alone, as then N is unknown.)
421 * - You must only transmit handles as part of handle-containing messages. Anything else is undefined behavior.
422 * - Let M be a given handle-containing message with blob B of size N; and handle H.
423 * Let a *write op* be nb_write_some_with_native_handle() (or an async_write_with_native_handle() based on it).
424 * - You shall attempt one write op for the blob B of size N together with handle H. Do *not* intermix it with any
425 * other bytes or handles.
426 * - In the non-blocking write op case (nb_write_some_with_native_handle()) it may yield successfully sending
427 * N' bytes, where 1 <= N' < N. This means the handle was successfully sent also, because the handle is
428 * associated with the *first byte* of the write -- and read -- op. If this happens, don't worry about it;
429 * continue with the rest of the protocol, including sending at least the remaining (N - N') bytes of M.
430 * - On the receiver side, you must symmetrically execute the read op (nb_read_some_with_native_handle(), perhaps
431 * after a `Peer_socket::async_wait()`) to attempt receipt of all of M, including supplying a target
432 * blob of exactly N bytes -- without mixing with any other bytes or handles. The "hard" part of this is mainly
433 * to avoid having the previous read op "cut into" M.
434 * - (Informally, a common-sense way to do it just make
435 * your protocol message-based, such that the length of the next message is always known on either side.)
436 * - Again, if the nb_read_some_with_native_handle() call returns N', where 1 <= N' < N, then no worries.
437 * The handle S *will* have been successfully received, being associated with byte 1 of M.
438 * Keep reading the rest of M (namely, the remaining (N - N') bytes of the blob B) with more read op(s).
439 * - (To put a fine point on it: In known Linux versions as of this writing, if you do try to read-op N' bytes
440 * having executed write-op with N'' bytes, where N'' > N', then you may observe very strange, undefined
441 * (albeit non-crashy), behavior such as S disappearing or replacing a following-message handle S'. Don't.)
442 *
443 * That is admittedly many words, but really in practice it's fairly natural and simple to design a message-based
444 * protocol and implementation around it. Just do follow these; I merely wanted to be complete.
445 *
446 * ### Features of `Peer_socket::read_some()` not provided here ###
447 * We have (consciously) made these concessions: ...see nb_write_some_with_native_handle() doc header. All of the
448 * listed omitted features have common-sense counterparts in the case of the present function.
449 *
450 * ### Advanced feature on top of `Peer_socket::read_some()` ###
451 * Lastly, `Peer_socket::receive()` is identical to `Peer_socket::read_some()` but adds an overload wherein one can
452 * pass in a (largely unportable, I (ygoldfel) think) `message_flags` bit mask. We *do* provide a close cousin of this
453 * feature via the (optional as of this writing) arg `native_recvmsg_flags`. (Rationale: We specifically omitted it
454 * in nb_write_some_with_native_handle(); yet add it here because the value `MSG_CMSG_CLOEXEC` has specific utility.)
455 * One may override the default (`0`) by supplying any value one would supply as the `int flags` arg of Linux's
456 * `recvmsg()` (see `man recvmsg`). As one can see in the `man` page, this is a bit mask or ORed values. Formally,
457 * however, the only value supported (does not lead to undefined behavior) is `MSG_CMSG_CLOEXEC` (please read its docs
458 * elsewhere, but in summary it sets the close-on-`exec` bit of any non-null received `*target_payload_blob`).
459 * Informally: that flag may be important for one's application; so we provide for it; however beyond that one please
460 * refer to the reasoning regarding not supporting `message_flags` in nb_write_some_with_native_handle() as explained
461 * in its doc header.
462 *
463 * ### Rationale ###
464 * This function exists because... [text omitted -- same reasoning as similar rationale for
465 * async_write_with_native_handle()].
466 *
467 * @param logger_ptr
468 * Logger to use for subsequently logging.
469 * @param peer_socket
470 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
471 * attempting `read_some()` on such a socket. If null behavior is undefined (assertion may trip).
472 * @param target_payload_hndl
473 * The native handle wrapper into which to copy the received handle; it shall be set such that
474 * `target_payload_hndl->null() == true` if the read-op returned 1+ (succeeded), but those bytes were not
475 * accompanied by any native handle. It shall also be thus set if 0 is returned (indicating error
476 * including would-block and fatal errors).
477 * @param target_payload_blob
478 * The buffer into which to write received blob data, namely up to `target_payload_blob->size()` bytes.
479 * If null, or the size is not positive, behiavor is undefined (assertion may trip).
480 * Reiterating the above outcome semantics: Either there is no error, and then the `N` returned will be 1+; or a
481 * truthy `Error_code` is returned either via the out-arg or via thrown `Runtime_error`, and in the former case
482 * 0 is returned.
483 * @param err_code
484 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
485 * `boost::asio::error::would_block` (socket not writable, likely because other side isn't reading ASAP),
486 * ipc::transport::error::Code::S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL
487 * (strictly more than 1 handle detected in the read-op, but we support only 1 at this time; see above;
488 * maybe they didn't use above write-op function(s) and/or didn't follow anti-straddling suggestion above),
489 * other system codes (see notes above in the outcome discussion).
490 * @param message_flags
491 * See boost.asio `Peer_socket::receive()` overload with this arg.
492 * @return 0 if non-null `err_code` and truthy resulting `*err_code`, and hence no bytes or the handle was sent; 1+
493 * if that number of bytes were sent plus the native handle (and hence falsy `*err_code` if non-null).
494 *
495 * @internal
496 * ### Implementation notes ###
497 * Where does the content of "Blob/handle semantics" originate? Answer: Good question, as reading `man` pages to do
498 * with `sendmsg()/recvmsg()/cmsg/unix`, etc., gives hints but really is incomplete and certainly not formally complete.
499 * Without such a description, one can guess at how `SOL_SOCKET/SCM_RIGHTS` (sending of FDs along with blobs) might work
500 * but not conclusively. I (ygoldfel) nevertheless actually correctly developed the relevant conclusions via common
501 * sense/experience... and *later* confirmed them by reading kernel source and the delightfully helpful
502 * write-up at [ https://gist.github.com/kentonv/bc7592af98c68ba2738f4436920868dc ] (Googled "SCM_RIGHTS gist").
503 * Reading these may give the code inspector/maintainer (you?) more peace of mind. Basically, though, the key gist
504 * is:
505 * - The handle(s) are associated with byte 1 of the blob given to the `sendmsg()` call containing those handle(s).
506 * For this reason, to avoid protocol chaos, you should send each given handle with the same "synchronized" byte
507 * on both sides.
508 * - The length of that blob similarly matters -- which is not normal, as otherwise message boundaries are *not*
509 * normally maintained for stream connections -- and for this reason the read op must accept a result into a blob
510 * of at *least* the same same size as the corresponding write op. (For simplicity and other reasons my
511 * instructions say it should just be equal.)
512 *
513 * Lastly, I note that potentially using `SOCK_SEQPACKET` (which purports to conserve message boundaries at all times)
514 * instead of `SOCK_STREAM` (which we use) might remove all ambiguity. On the other hand it's barely documented itself.
515 * The rationale for the decision to use `SOCK_STREAM` is discussed elsewhere; this note is to reassure that I
516 * (ygoldfel) don't quite find the above complexity reason enough to switch to `SOCK_SEQPACKET`.
517 */
518size_t nb_read_some_with_native_handle(flow::log::Logger* logger_ptr,
519 Peer_socket* peer_socket,
520 Native_handle* target_payload_hndl,
521 const util::Blob_mutable& target_payload_blob,
522 Error_code* err_code,
523 int message_flags = 0);
524
525/**
526 * boost.asio extension similar to
527 * `boost::asio::async_read(Peer_socket&, Blob_mutable, Task_err_sz)` with the difference that the target
528 * buffer (util::Blob_mutable) is determined by calling the arg-supplied function at the time when at least 1 byte
529 * is available to read, instead of the buffer being given direcly as an arg. By determining where to target when
530 * there are actual data available, one can avoid unnecessary copying in the meantime; among other applications.
531 *
532 * ### Behavior ###
533 * boost.asio's `async_read()` free function is generically capable of receiving into a sequence of 1+ buffers on
534 * a connected stream socket, continuing until either the entire sequence is fully filled to the byte; or an error (not
535 * counting would-block, which just means keep trying to make progress when possible). We act exactly the same with
536 * the following essential differences being the exceptions:
537 * - The target buffer is determined once system indicates at least 1 byte of data is available to actually read
538 * off socket; it's not supplied as an arg.
539 * - To determine it, we call `target_payload_blob_func()` which must return the util::Blob_mutable.
540 * - One can cancel the (rest of the) operation via `should_interrupt_func()`. This is called just after
541 * being internally informed the socket is ready for reading, so just before the 1st `target_payload_blob_func()`
542 * call, and ahead of each subsequent burst of non-blockingly-available bytes as well.
543 * If it returns `true`, then the operation is canceled; the target buffer (if it has even been determined)
544 * is not written to further; and `on_rcvd_or_error()` is never invoked. Note that `should_interrupt_func()`
545 * may be called ages after whatever outside interrupt-causing condition has occurred; typically your
546 * impl would merely check for that condition being the case (e.g., "have we encountered idle timeout earlier?").
547 * - Once the handler is called, its signature is similar (`Error_code`, `size_t` of bytes received) but with one
548 * added arg, `const Blob_mutable& target_payload_blob`, which is simply a copy of the light-weight object returned
549 * per preceding bullet point.
550 * - It is possible `target_payload_blob_func()` is never called; in this case the error code shall be truthy, and
551 * the reported size received shall be 0. In this case disregard the `target_payload_blob` value received; it
552 * shall be a null/empty blob.
553 * - The returned util::Blob_mutable may have `.size() == 0`. In this case we shall perform no actual read;
554 * and will simply invoke the handler immediately upon detecting this; the reported error code shall be falsy,
555 * and the reported size received shall be 0.
556 * - If `peer_socket->non_blocking() == false` at entry to this function, it shall be `true`
557 * at entry to the handler, except it *might* be false if the handler receives a truthy `Error_code`.
558 * (Informally: In that case, the connection should be considered hosed in any case and must not be used for
559 * traffic.)
560 * - More logging, as a convenience.
561 *
562 * It is otherwise identical... but certain aspects of `async_read()` are not included in the present
563 * function, however, though merely because they were not necessary as of this writing and hence excluded for
564 * simplicity; these are formally described below.
565 *
566 * ### Thread safety ###
567 * Like `async_read()`, the (synchronous) call is not thread-safe with respect to the given `*peer_socket` against
568 * all/most calls operating on the same object. Moreover, this extends to the brief time period when the first byte
569 * is available. Since there is no way of knowing when that might be, essentially you should consider this entire
570 * async op as not safe for concurrent execution with any/most calls operating on the same `Peer_socket`, in the
571 * time period [entry to async_read_with_target_func(), entry to `target_payload_blob_func()`].
572 *
573 * Informally, as with `async_read()`, it is unwise to do anything with `*peer_socket` upon calling the present
574 * function through when the handler begins executing.
575 *
576 * `on_rcvd_or_error()` is invoked fully respecting any possible executor (typically none, else a `Strand`) associated
577 * (via `bind_executor()` usually) with it.
578 *
579 * However `should_interrupt_func()` and `target_payload_blob()` are invoked directly via
580 * `peer_socket->get_executor()`, and any potential associated executor on these functions themselves is
581 * ignored. (This is the case simply because there was no internal-to-rest-of-Flow-IPC use case for acting otherwise;
582 * but it's conceivable to implement it later, albeit at the cost of some more processor cycles used.)
583 *
584 * ### Features of `boost::asio::async_read()` not provided here ###
585 * We have (consciously) made these concessions: ...see async_write_with_native_handle() doc header. All of the
586 * listed omitted features have common-sense counterparts in the case of the present function. In addition:
587 * - `peer_socket` has to be a socket of that specific type, a local stream socket. `async_write()` is templated on
588 * the socket type and will work with other connected stream sockets, notably TCP.
589 *
590 * ### Rationale ###
591 * - This function exists because... [text omitted -- same reasoning as similar rationale for
592 * async_write_with_native_handle()].
593 * - Namely, though, the main thing is being able to delay targeting the data until that data are actually
594 * available to avoid internal copying in Native_socket_stream internals.
595 * - The `should_interrupt_func()` feature was necessary because Native_socket_stream internals has a condition
596 * where it is obligated to stop writing to the user buffer and return to them an overall in-pipe error:
597 * - idle timeout (no in-traffic for N time).
598 * - But the whole point of `async_read()` and therefore the present extension is to keep reading until all N
599 * bytes are here, or an error. The aforementioned condition is, in a sense, the latter: an error; except
600 * it originates locally. So `should_interrupt_func()` is a way to signal this.
601 * - As noted, this sets non-blocking mode on `*peer_socket` if needed. It does not "undo" this. Why not?
602 * After all a clean op would act as if it has nothing to do with this. Originally I (ygoldfel) did "undo" it.
603 * Then I decided otherwise for 2 reasons. 1, the situation where the "undo" itself fails made it ambiguous how to
604 * report this through the handler if everything had worked until then (unlikely as that is). 2, in coming up
605 * with a decent semantic approach for this annoying corner case that'll never really happen, and then thinking of
606 * how to document it, I realized the following practical truths: `Peer_socket::non_blocking()` mode affects only
607 * the non-`async*()` ops on `Peer_socket`, and it's common to want those to be non-blocking in the first place,
608 * if one also feels the need to use `async*()` (like the present function). So why jump through hoops for purity's
609 * sake? Of course this can be changed later.
610 *
611 * @tparam Task_err_blob
612 * See `on_rcvd_or_error` arg.
613 * @tparam Target_payload_blob_func
614 * See `target_payload_blob_func` arg.
615 * @tparam Should_interrupt_func
616 * See `should_interrupt_func` arg.
617 * @param logger_ptr
618 * Logger to use for subsequently logging.
619 * @param peer_socket
620 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
621 * attempting `async_read()` on such a socket. If null behavior is undefined (assertion may trip).
622 * @param on_rcvd_or_error
623 * Handler to execute at most once on completion
624 * of the async op. It is executed as if via `post(peer_socket->get_executor())`, fully respecting
625 * any executor bound to it (via `bind_executor()`, such as a strand).
626 * It shall be passed, in this order, `Error_code` and a value equal to the one returned by
627 * `target_payload_blob_func()` earlier. The semantics of the first value is identical to that
628 * for `boost::asio::async_read()`.
629 * @param target_payload_blob_func
630 * Function to execute at most once, when it is
631 * first indicated by the system that data might be available on the connection. It is executed as if via
632 * `post(peer_socket->get_executor())`, but any executor bound to it (via `bind_executor()` is ignored).
633 * It takes no args and shall return `util::Blob_mutable` object
634 * describing the memory area into which we shall write on successful receipt of data.
635 * It will not be invoked at all, among other reasons, if `should_interrupt_func()` returns `true` the
636 * first time *it* is invoked.
637 * @param should_interrupt_func
638 * Function to execute ahead of nb-reading arriving data (copying it from kernel buffer to
639 * the target buffer); hence the general loop is await-readable/call-this-function/nb-read/repeat
640 * (until the buffer is filled, there is an error, or `should_interrupt_func()` returns `true`).
641 * It is executed as if via `post(peer_socket->get_executor())`, but any executor bound to it (via
642 * `bind_executor()` is ignored). It takes no args and shall return
643 * `bool` specifying whether to proceed with the operation (`false`) or to interrupt the whole thing (`true`).
644 * If it returns `true` then async_read_with_target_func() will *not* call `on_rcvd_or_error()`.
645 * Instead the user should consider `should_interrupt_func()` itself as the completion handler being invoked.
646 */
647template<typename Task_err_blob, typename Target_payload_blob_func, typename Should_interrupt_func>
649 (flow::log::Logger* logger_ptr,
650 Peer_socket* peer_socket,
651 Target_payload_blob_func&& target_payload_blob_func,
652 Should_interrupt_func&& should_interrupt_func,
653 Task_err_blob&& on_rcvd_or_error);
654
655/**
656 * boost.asio extension similar to
657 * `boost::asio::async_read(Peer_socket&, Blob_mutable, Task_err_sz)` with the difference that it can be
658 * canceled/interrupted via `should_interrupt_func()` in the same way as the otherwise more
659 * complex async_read_with_target_func().
660 *
661 * So think of it as either:
662 * - `async_read()` with added `should_interrupt_func()` feature; or
663 * - `async_read_with_target_func()` minus `target_payload_blob_func()` feature. Or formally, it's as-if
664 * that one was used but with a `target_payload_blob_func()` that simply returns `target_payload_blob`.
665 *
666 * Omitting detailed comments already present in async_read_with_target_func() doc header; just skip the
667 * parts to do with `target_payload_blob_func()`.
668 *
669 * @tparam Task_err
670 * See `on_rcvd_or_error` arg.
671 * @tparam Should_interrupt_func
672 * See async_read_with_target_func().
673 * @param logger_ptr
674 * See async_read_with_target_func().
675 * @param peer_socket
676 * See async_read_with_target_func().
677 * @param on_rcvd_or_error
678 * The completion handler; it is passed either the success code (all requested bytes were read),
679 * or an error code (pipe is hosed).
680 * @param target_payload_blob
681 * `util::Blob_mutable` object
682 * describing the memory area into which we shall write on successful receipt of data.
683 * @param should_interrupt_func
684 * See async_read_with_target_func().
685 */
686template<typename Task_err, typename Should_interrupt_func>
688 (flow::log::Logger* logger_ptr, Peer_socket* peer_socket, util::Blob_mutable target_payload_blob,
689 Should_interrupt_func&& should_interrupt_func, Task_err&& on_rcvd_or_error);
690
691/**
692 * Little utility that returns the raw Native_handle suitable for #Peer_socket to the OS.
693 * This is helpful to close, without invoking a native API (`close()` really), a value returned by
694 * `Peer_socket::release()` or, perhaps, received over a `Native_socket_stream`.
695 *
696 * The in-arg is nullified (it becomes `.null()`).
697 *
698 * Nothing is logged; no errors are emitted. This is intended for no-questions-asked cleanup.
699 *
700 * @param peer_socket_native_or_null
701 * The native socket to close. No-op (not an error) if it is `.null()`.
702 * If not `.null()`, `peer_socket_native_or_null.m_native_handle` must be suitable for
703 * `Peer_socket::native_handle()`. In practice the use case informing release_native_peer_socket()
704 * is: `peer_socket_native = p.release()`, where `p` is a `Peer_socket`. Update:
705 * Another use case came about: receiving `peer_socket_native` over a `Native_socket_stream`.
706 */
707void release_native_peer_socket(Native_handle&& peer_socket_native_or_null);
708
709} // namespace ipc::transport::asio_local_stream_socket
Gettable (read-only) socket option for use with asio_local_stream_socket::Peer_socket ....
Additional (versus boost.asio) APIs for advanced work with local stream (Unix domain) sockets includi...
Protocol::endpoint Endpoint
Short-hand for boost.asio Unix domain peer stream-socket endpoint.
local_ns::stream_protocol Protocol
Short-hand for boost.asio Unix domain stream-socket protocol.
size_t nb_write_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket *peer_socket_ptr, Native_handle payload_hndl, const util::Blob_const &payload_blob, Error_code *err_code)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->write_some(pay...
Protocol::acceptor Acceptor
Short-hand for boost.asio Unix domain stream-socket acceptor (listening guy) socket.
Protocol::socket Peer_socket
Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
void async_write_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket *peer_socket_ptr, Native_handle payload_hndl, const util::Blob_const &payload_blob, Task_err &&on_sent_or_error)
boost.asio extension similar to boost::asio::async_write(Peer_socket&, Blob_const,...
size_t nb_read_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket *peer_socket_ptr, Native_handle *target_payload_hndl_ptr, const util::Blob_mutable &target_payload_blob, Error_code *err_code, int message_flags)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->read_some(targ...
void async_read_with_target_func(flow::log::Logger *logger_ptr, Peer_socket *peer_socket, Target_payload_blob_func &&target_payload_blob_func, Should_interrupt_func &&should_interrupt_func, Task_err_blob &&on_rcvd_or_error)
boost.asio extension similar to boost::asio::async_read(Peer_socket&, Blob_mutable,...
void async_read_interruptible(flow::log::Logger *logger_ptr, Peer_socket *peer_socket, util::Blob_mutable target_payload_blob, Should_interrupt_func &&should_interrupt_func, Task_err &&on_rcvd_or_error)
boost.asio extension similar to boost::asio::async_read(Peer_socket&, Blob_mutable,...
void release_native_peer_socket(Native_handle &&peer_socket_native_or_null)
Little utility that returns the raw Native_handle suitable for Peer_socket to the OS.
boost::asio::mutable_buffer Blob_mutable
Short-hand for an mutable blob somewhere in memory, stored as exactly a void* and a size_t.
Definition: util_fwd.hpp:134
boost::asio::const_buffer Blob_const
Short-hand for an immutable blob somewhere in memory, stored as exactly a void const * and a size_t.
Definition: util_fwd.hpp:128
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:297
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.