Flow-IPC 1.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
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 * @see Please read the "Blob/handle semantics" about working with native
345 * handle accompaniment, in the nb_read_some_with_native_handle() doc header.
346 *
347 * Certain aspects of `Peer_socket::read_some()` are not included in the present function, however, though merely
348 * because they were not necessary as of this writing and hence excluded for simplicity; these are formally described
349 * below.
350 *
351 * ### Formal behavior ###
352 * This function requires that `target_payload_blob` be non-empty. It shall at entry set `*target_payload_hndl`
353 * so that `target_payload_hndl->null() == true`. `target_payload_hndl` (the pointer) must not be null.
354 * (If you want to receive into non-empty buffer but expect no handle, then just use boost.asio
355 * `Peer_socket::read_some()`. If you want to receive a non-empty buffer but no handle, then just use
356 * boost.asio `async_write()`. As of this writing the relevant OS shall not support receiving a handle but a
357 * null buffer.)
358 *
359 * The function exits without blocking. The receiving occurs synchronously, if possible, or does not occur otherwise.
360 * A successful operation is defined as receiving 1+ bytes into the blob; and the native handle if it was present.
361 * It is not possible that a native handle is received but 0 bytes of the blob are. See `flow::Error_code` docs for
362 * error reporting semantics (if `err_code` is non-null, `*err_code` is set to code or success; else exception with
363 * that code is thrown in the former [non-success] case).
364 *
365 * The following are all the possible outcomes:
366 * - `N > 0` is returned; and `*err_code == Error_code()` if non-null.
367 * This indicates 1 or more (`N`) bytes were placed at the start of the buffer, and *if* exactly 1 native handle
368 * handle was transmitted along with some subset of those `N` bytes, *then* it was successfully received into
369 * `*target_payload_hndl`; or else the fact there were exactly 0 such handles was successfully determined and
370 * reflected via `target_payload_hndl->null() == true`.
371 * If `N != target_payload_blob.size()`, then no further bytes can currently be read without blocking and should
372 * be tried later if desired. (Informally: In that case consider `Peer_socket::async_wait()` followed by retrying
373 * the present function, in that case.)
374 * - If non-null `err_code`, then `N == 0` is returned; and `*err_code == E` is set to the triggering problem.
375 * If null, then flow::error::Runtime_error is thrown containing `Error_code E`.
376 * - `E == boost::asio::error::would_block` specifically indicates the non-fatal condition wherein `*peer_socket`
377 * cannot currently receive, until it reaches readable state again (i.e., bytes and possibly handle arrive from
378 * peer).
379 * - Other `E` values indicate the connection is (potentially gracefully) permanently incapable of transmission.
380 * - In particular `E == boost::asio::error::eof` indicates the connection was gracefully closed by peer.
381 * (Informally, this is usually not to be treated differently from other fatal errors like
382 * `boost::asio::error::connection_reset`.)
383 * - It may be tempting to distinguish between "true" system errors (probably from `boost::asio::error::`)
384 * and "protocol" errors from `ipc::transport::error::Code` (as of this writing
385 * `S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL`): one *can* technically keep reading in the latter
386 * case, in that the underlying connection is still connected potentially. However, formally, behavior is
387 * undefined if one reads more subsequently. Informally: if the other side has violated protocol
388 * expectations -- or if your side has violated expectations on proper reading (see below section on that) --
389 * then neither side can be trusted to recover logical consistency and must abandon the connection.
390 * - `E == operation_aborted` is not possible.
391 *
392 * Items are extensively logged on `*logger_ptr`, and we follow the normal best practices to avoid verbose messages
393 * at the severities strictly higher than `TRACE`. In particular, any error is logged as a `WARNING`, so in particular
394 * there's no need for caller to specifically log about the details of a non-false `E`.
395 * (If this is still too slow, you may use the `flow::log::Config::this_thread_verbosity_override_auto()` to
396 * temporarily, in that thread only, disable logging. This is quite easy and performant.)
397 *
398 * ### Blob/handle semantics ###
399 * Non-blocking stream-blob-send/receive semantics must be familiar to you, such as from TCP and otherwise.
400 * By adding native handles (further, just *handles*) as accompaniment to this system, non-trivial -- arguably
401 * subtle -- questions are raised about how it all works together. The central question is, perhaps, if I ask to send
402 * N bytes and handle S, non-blockingly, what are the various possibilities for sending less than N bytes of the blob
403 * and whether S is also sent? Conversely, how will receiving on the other side work? The following describes those
404 * semantics and *mandates* how to properly handle it. Not following these formally leads to undefined behavior.
405 * (Informally, for the tested OS versions, it is possible to count on certain additional behaviors; but trying to do
406 * so is (1) prone to spurious changes and breakage in different OS versions and types, since much of this is
407 * undocumented; and (2) will probably just make your life *more* difficult anyway, not less. Honestly I (ygoldfel)
408 * designed it for ease of following as opposed to exactly maximal freedom of capability. So... just follow these.)
409 *
410 * Firstly, as already stated, it is not possible to try sending a handle sans a blob; and it is not possible
411 * to receive a handle sans a blob. (The converse is a regular non-blocking blob write or receive op.)
412 *
413 * Secondly, one must think of the handle as associated with *exactly the first byte* of the blob arg to the
414 * write call (nb_write_some_with_native_handle()). Similarly, one must think of the handle as associated with
415 * *exactly the first byte* of the blob arg to the read call (nb_read_some_with_native_handle()). Moreover,
416 * known OS make certain not-well-documented assumptions about message lengths. What does this all
417 * mean in practice?
418 * - You may design your protocol however you want, except the following requirement: Define a
419 * *handle-containing message* as a combination of a blob of 1+ bytes of some known (on both sides, at the time
420 * of both sending and receipt) length N *and* exactly *one* handle. You must aim to send this message and
421 * receive it exactly as sent, meaning with message boundaries respected. (To be clear, you're free to use
422 * any technique to make N known on both sides; e.g., it may be a constant; or it may be passed in a previous
423 * message. However, it's not compatible with using a sentinel alone, as then N is unknown.)
424 * - You must only transmit handles as part of handle-containing messages. Anything else is undefined behavior.
425 * - Let M be a given handle-containing message with blob B of size N; and handle H.
426 * Let a *write op* be nb_write_some_with_native_handle() (or an async_write_with_native_handle() based on it).
427 * - You shall attempt one write op for the blob B of size N together with handle H. Do *not* intermix it with any
428 * other bytes or handles.
429 * - In the non-blocking write op case (nb_write_some_with_native_handle()) it may yield successfully sending
430 * N' bytes, where 1 <= N' < N. This means the handle was successfully sent also, because the handle is
431 * associated with the *first byte* of the write -- and read -- op. If this happens, don't worry about it;
432 * continue with the rest of the protocol, including sending at least the remaining (N - N') bytes of M.
433 * - On the receiver side, you must symmetrically execute the read op (nb_read_some_with_native_handle(), perhaps
434 * after a `Peer_socket::async_wait()`) to attempt receipt of all of M, including supplying a target
435 * blob of exactly N bytes -- without mixing with any other bytes or handles. The "hard" part of this is mainly
436 * to avoid having the previous read op "cut into" M.
437 * - (Informally, a common-sense way to do it just make
438 * your protocol message-based, such that the length of the next message is always known on either side.)
439 * - Again, if the nb_read_some_with_native_handle() call returns N', where 1 <= N' < N, then no worries.
440 * The handle S *will* have been successfully received, being associated with byte 1 of M.
441 * Keep reading the rest of M (namely, the remaining (N - N') bytes of the blob B) with more read op(s).
442 * - (To put a fine point on it: In known Linux versions as of this writing, if you do try to read-op N' bytes
443 * having executed write-op with N'' bytes, where N'' > N', then you may observe very strange, undefined
444 * (albeit non-crashy), behavior such as S disappearing or replacing a following-message handle S'. Don't.)
445 *
446 * That is admittedly many words, but really in practice it's fairly natural and simple to design a message-based
447 * protocol and implementation around it. Just do follow these; I merely wanted to be complete.
448 *
449 * ### Features of `Peer_socket::read_some()` not provided here ###
450 * We have (consciously) made these concessions: ...see nb_write_some_with_native_handle() doc header. All of the
451 * listed omitted features have common-sense counterparts in the case of the present function.
452 *
453 * ### Advanced feature on top of `Peer_socket::read_some()` ###
454 * Lastly, `Peer_socket::receive()` is identical to `Peer_socket::read_some()` but adds an overload wherein one can
455 * pass in a (largely unportable, I (ygoldfel) think) `message_flags` bit mask. We *do* provide a close cousin of this
456 * feature via the (optional as of this writing) arg `native_recvmsg_flags`. (Rationale: We specifically omitted it
457 * in nb_write_some_with_native_handle(); yet add it here because the value `MSG_CMSG_CLOEXEC` has specific utility.)
458 * One may override the default (`0`) by supplying any value one would supply as the `int flags` arg of Linux's
459 * `recvmsg()` (see `man recvmsg`). As one can see in the `man` page, this is a bit mask or ORed values. Formally,
460 * however, the only value supported (does not lead to undefined behavior) is `MSG_CMSG_CLOEXEC` (please read its docs
461 * elsewhere, but in summary it sets the close-on-`exec` bit of any non-null received `*target_payload_blob`).
462 * Informally: that flag may be important for one's application; so we provide for it; however beyond that one please
463 * refer to the reasoning regarding not supporting `message_flags` in nb_write_some_with_native_handle() as explained
464 * in its doc header.
465 *
466 * ### Rationale ###
467 * This function exists because... [text omitted -- same reasoning as similar rationale for
468 * async_write_with_native_handle()].
469 *
470 * @param logger_ptr
471 * Logger to use for subsequently logging.
472 * @param peer_socket
473 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
474 * attempting `read_some()` on such a socket. If null behavior is undefined (assertion may trip).
475 * @param target_payload_hndl
476 * The native handle wrapper into which to copy the received handle; it shall be set such that
477 * `target_payload_hndl->null() == true` if the read-op returned 1+ (succeeded), but those bytes were not
478 * accompanied by any native handle. It shall also be thus set if 0 is returned (indicating error
479 * including would-block and fatal errors).
480 * @param target_payload_blob
481 * The buffer into which to write received blob data, namely up to `target_payload_blob->size()` bytes.
482 * If null, or the size is not positive, behiavor is undefined (assertion may trip).
483 * Reiterating the above outcome semantics: Either there is no error, and then the `N` returned will be 1+; or a
484 * truthy `Error_code` is returned either via the out-arg or via thrown `Runtime_error`, and in the former case
485 * 0 is returned.
486 * @param err_code
487 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
488 * `boost::asio::error::would_block` (socket not writable, likely because other side isn't reading ASAP),
489 * ipc::transport::error::Code::S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL
490 * (strictly more than 1 handle detected in the read-op, but we support only 1 at this time; see above;
491 * maybe they didn't use above write-op function(s) and/or didn't follow anti-straddling suggestion above),
492 * other system codes (see notes above in the outcome discussion).
493 * @param message_flags
494 * See boost.asio `Peer_socket::receive()` overload with this arg.
495 * @return 0 if non-null `err_code` and truthy resulting `*err_code`, and hence no bytes or the handle was sent; 1+
496 * if that number of bytes were sent plus the native handle (and hence falsy `*err_code` if non-null).
497 *
498 * @internal
499 * ### Implementation notes ###
500 * Where does the content of "Blob/handle semantics" originate? Answer: Good question, as reading `man` pages to do
501 * with `sendmsg()/recvmsg()/cmsg/unix`, etc., gives hints but really is incomplete and certainly not formally complete.
502 * Without such a description, one can guess at how `SOL_SOCKET/SCM_RIGHTS` (sending of FDs along with blobs) might work
503 * but not conclusively. I (ygoldfel) nevertheless actually correctly developed the relevant conclusions via common
504 * sense/experience... and *later* confirmed them by reading kernel source and the delightfully helpful
505 * write-up at [ https://gist.github.com/kentonv/bc7592af98c68ba2738f4436920868dc ] (Googled "SCM_RIGHTS gist").
506 * Reading these may give the code inspector/maintainer (you?) more peace of mind. Basically, though, the key gist
507 * is:
508 * - The handle(s) are associated with byte 1 of the blob given to the `sendmsg()` call containing those handle(s).
509 * For this reason, to avoid protocol chaos, you should send each given handle with the same "synchronized" byte
510 * on both sides.
511 * - The length of that blob similarly matters -- which is not normal, as otherwise message boundaries are *not*
512 * normally maintained for stream connections -- and for this reason the read op must accept a result into a blob
513 * of at *least* the same same size as the corresponding write op. (For simplicity and other reasons my
514 * instructions say it should just be equal.)
515 *
516 * Lastly, I note that potentially using `SOCK_SEQPACKET` (which purports to conserve message boundaries at all times)
517 * instead of `SOCK_STREAM` (which we use) might remove all ambiguity. On the other hand it's barely documented itself.
518 * The rationale for the decision to use `SOCK_STREAM` is discussed elsewhere; this note is to reassure that I
519 * (ygoldfel) don't quite find the above complexity reason enough to switch to `SOCK_SEQPACKET`.
520 */
521size_t nb_read_some_with_native_handle(flow::log::Logger* logger_ptr,
522 Peer_socket* peer_socket,
523 Native_handle* target_payload_hndl,
524 const util::Blob_mutable& target_payload_blob,
525 Error_code* err_code,
526 int message_flags = 0);
527
528/**
529 * boost.asio extension similar to
530 * `boost::asio::async_read(Peer_socket&, Blob_mutable, Task_err_sz)` with the difference that the target
531 * buffer (util::Blob_mutable) is determined by calling the arg-supplied function at the time when at least 1 byte
532 * is available to read, instead of the buffer being given direcly as an arg. By determining where to target when
533 * there are actual data available, one can avoid unnecessary copying in the meantime; among other applications.
534 *
535 * ### Behavior ###
536 * boost.asio's `async_read()` free function is generically capable of receiving into a sequence of 1+ buffers on
537 * a connected stream socket, continuing until either the entire sequence is fully filled to the byte; or an error (not
538 * counting would-block, which just means keep trying to make progress when possible). We act exactly the same with
539 * the following essential differences being the exceptions:
540 * - The target buffer is determined once system indicates at least 1 byte of data is available to actually read
541 * off socket; it's not supplied as an arg.
542 * - To determine it, we call `target_payload_blob_func()` which must return the util::Blob_mutable.
543 * - One can cancel the (rest of the) operation via `should_interrupt_func()`. This is called just after
544 * being internally informed the socket is ready for reading, so just before the 1st `target_payload_blob_func()`
545 * call, and ahead of each subsequent burst of non-blockingly-available bytes as well.
546 * If it returns `true`, then the operation is canceled; the target buffer (if it has even been determined)
547 * is not written to further; and `on_rcvd_or_error()` is never invoked. Note that `should_interrupt_func()`
548 * may be called ages after whatever outside interrupt-causing condition has occurred; typically your
549 * impl would merely check for that condition being the case (e.g., "have we encountered idle timeout earlier?").
550 * - Once the handler is called, its signature is similar (`Error_code`, `size_t` of bytes received) but with one
551 * added arg, `const Blob_mutable& target_payload_blob`, which is simply a copy of the light-weight object returned
552 * per preceding bullet point.
553 * - It is possible `target_payload_blob_func()` is never called; in this case the error code shall be truthy, and
554 * the reported size received shall be 0. In this case disregard the `target_payload_blob` value received; it
555 * shall be a null/empty blob.
556 * - The returned util::Blob_mutable may have `.size() == 0`. In this case we shall perform no actual read;
557 * and will simply invoke the handler immediately upon detecting this; the reported error code shall be falsy,
558 * and the reported size received shall be 0.
559 * - If `peer_socket->non_blocking() == false` at entry to this function, it shall be `true`
560 * at entry to the handler, except it *might* be false if the handler receives a truthy `Error_code`.
561 * (Informally: In that case, the connection should be considered hosed in any case and must not be used for
562 * traffic.)
563 * - More logging, as a convenience.
564 *
565 * It is otherwise identical... but certain aspects of `async_read()` are not included in the present
566 * function, however, though merely because they were not necessary as of this writing and hence excluded for
567 * simplicity; these are formally described below.
568 *
569 * ### Thread safety ###
570 * Like `async_read()`, the (synchronous) call is not thread-safe with respect to the given `*peer_socket` against
571 * all/most calls operating on the same object. Moreover, this extends to the brief time period when the first byte
572 * is available. Since there is no way of knowing when that might be, essentially you should consider this entire
573 * async op as not safe for concurrent execution with any/most calls operating on the same `Peer_socket`, in the
574 * time period [entry to async_read_with_target_func(), entry to `target_payload_blob_func()`].
575 *
576 * Informally, as with `async_read()`, it is unwise to do anything with `*peer_socket` upon calling the present
577 * function through when the handler begins executing.
578 *
579 * `on_rcvd_or_error()` is invoked fully respecting any possible executor (typically none, else a `Strand`) associated
580 * (via `bind_executor()` usually) with it.
581 *
582 * However `should_interrupt_func()` and `target_payload_blob()` are invoked directly via
583 * `peer_socket->get_executor()`, and any potential associated executor on these functions themselves is
584 * ignored. (This is the case simply because there was no internal-to-rest-of-Flow-IPC use case for acting otherwise;
585 * but it's conceivable to implement it later, albeit at the cost of some more processor cycles used.)
586 *
587 * ### Features of `boost::asio::async_read()` not provided here ###
588 * We have (consciously) made these concessions: ...see async_write_with_native_handle() doc header. All of the
589 * listed omitted features have common-sense counterparts in the case of the present function. In addition:
590 * - `peer_socket` has to be a socket of that specific type, a local stream socket. `async_write()` is templated on
591 * the socket type and will work with other connected stream sockets, notably TCP.
592 *
593 * ### Rationale ###
594 * - This function exists because... [text omitted -- same reasoning as similar rationale for
595 * async_write_with_native_handle()].
596 * - Namely, though, the main thing is being able to delay targeting the data until that data are actually
597 * available to avoid internal copying in Native_socket_stream internals.
598 * - The `should_interrupt_func()` feature was necessary because Native_socket_stream internals has a condition
599 * where it is obligated to stop writing to the user buffer and return to them an overall in-pipe error:
600 * - idle timeout (no in-traffic for N time).
601 * - But the whole point of `async_read()` and therefore the present extension is to keep reading until all N
602 * bytes are here, or an error. The aforementioned condition is, in a sense, the latter: an error; except
603 * it originates locally. So `should_interrupt_func()` is a way to signal this.
604 * - As noted, this sets non-blocking mode on `*peer_socket` if needed. It does not "undo" this. Why not?
605 * After all a clean op would act as if it has nothing to do with this. Originally I (ygoldfel) did "undo" it.
606 * Then I decided otherwise for 2 reasons. 1, the situation where the "undo" itself fails made it ambiguous how to
607 * report this through the handler if everything had worked until then (unlikely as that is). 2, in coming up
608 * with a decent semantic approach for this annoying corner case that'll never really happen, and then thinking of
609 * how to document it, I realized the following practical truths: `Peer_socket::non_blocking()` mode affects only
610 * the non-`async*()` ops on `Peer_socket`, and it's common to want those to be non-blocking in the first place,
611 * if one also feels the need to use `async*()` (like the present function). So why jump through hoops for purity's
612 * sake? Of course this can be changed later.
613 *
614 * @tparam Task_err_blob
615 * See `on_rcvd_or_error` arg.
616 * @tparam Target_payload_blob_func
617 * See `target_payload_blob_func` arg.
618 * @tparam Should_interrupt_func
619 * See `should_interrupt_func` arg.
620 * @param logger_ptr
621 * Logger to use for subsequently logging.
622 * @param peer_socket
623 * Pointer to stream socket. If it is not connected, or otherwise unsuitable, behavior is identical to
624 * attempting `async_read()` on such a socket. If null behavior is undefined (assertion may trip).
625 * @param on_rcvd_or_error
626 * Handler to execute at most once on completion
627 * of the async op. It is executed as if via `post(peer_socket->get_executor())`, fully respecting
628 * any executor bound to it (via `bind_executor()`, such as a strand).
629 * It shall be passed, in this order, `Error_code` and a value equal to the one returned by
630 * `target_payload_blob_func()` earlier. The semantics of the first value is identical to that
631 * for `boost::asio::async_read()`.
632 * @param target_payload_blob_func
633 * Function to execute at most once, when it is
634 * first indicated by the system that data might be available on the connection. It is executed as if via
635 * `post(peer_socket->get_executor())`, but any executor bound to it (via `bind_executor()` is ignored).
636 * It takes no args and shall return `util::Blob_mutable` object
637 * describing the memory area into which we shall write on successful receipt of data.
638 * It will not be invoked at all, among other reasons, if `should_interrupt_func()` returns `true` the
639 * first time *it* is invoked.
640 * @param should_interrupt_func
641 * Function to execute ahead of nb-reading arriving data (copying it from kernel buffer to
642 * the target buffer); hence the general loop is await-readable/call-this-function/nb-read/repeat
643 * (until the buffer is filled, there is an error, or `should_interrupt_func()` returns `true`).
644 * It is executed as if via `post(peer_socket->get_executor())`, but any executor bound to it (via
645 * `bind_executor()` is ignored). It takes no args and shall return
646 * `bool` specifying whether to proceed with the operation (`false`) or to interrupt the whole thing (`true`).
647 * If it returns `true` then async_read_with_target_func() will *not* call `on_rcvd_or_error()`.
648 * Instead the user should consider `should_interrupt_func()` itself as the completion handler being invoked.
649 */
650template<typename Task_err_blob, typename Target_payload_blob_func, typename Should_interrupt_func>
652 (flow::log::Logger* logger_ptr,
653 Peer_socket* peer_socket,
654 Target_payload_blob_func&& target_payload_blob_func,
655 Should_interrupt_func&& should_interrupt_func,
656 Task_err_blob&& on_rcvd_or_error);
657
658/**
659 * boost.asio extension similar to
660 * `boost::asio::async_read(Peer_socket&, Blob_mutable, Task_err_sz)` with the difference that it can be
661 * canceled/interrupted via `should_interrupt_func()` in the same way as the otherwise more
662 * complex async_read_with_target_func().
663 *
664 * So think of it as either:
665 * - `async_read()` with added `should_interrupt_func()` feature; or
666 * - `async_read_with_target_func()` minus `target_payload_blob_func()` feature. Or formally, it's as-if
667 * that one was used but with a `target_payload_blob_func()` that simply returns `target_payload_blob`.
668 *
669 * Omitting detailed comments already present in async_read_with_target_func() doc header; just skip the
670 * parts to do with `target_payload_blob_func()`.
671 *
672 * @tparam Task_err
673 * See `on_rcvd_or_error` arg.
674 * @tparam Should_interrupt_func
675 * See async_read_with_target_func().
676 * @param logger_ptr
677 * See async_read_with_target_func().
678 * @param peer_socket
679 * See async_read_with_target_func().
680 * @param on_rcvd_or_error
681 * The completion handler; it is passed either the success code (all requested bytes were read),
682 * or an error code (pipe is hosed).
683 * @param target_payload_blob
684 * `util::Blob_mutable` object
685 * describing the memory area into which we shall write on successful receipt of data.
686 * @param should_interrupt_func
687 * See async_read_with_target_func().
688 */
689template<typename Task_err, typename Should_interrupt_func>
691 (flow::log::Logger* logger_ptr, Peer_socket* peer_socket, util::Blob_mutable target_payload_blob,
692 Should_interrupt_func&& should_interrupt_func, Task_err&& on_rcvd_or_error);
693
694/**
695 * Little utility that returns the raw Native_handle suitable for #Peer_socket to the OS.
696 * This is helpful to close, without invoking a native API (`close()` really), a value returned by
697 * `Peer_socket::release()` or, perhaps, received over a `Native_socket_stream`.
698 *
699 * The in-arg is nullified (it becomes `.null()`).
700 *
701 * Nothing is logged; no errors are emitted. This is intended for no-questions-asked cleanup.
702 *
703 * @param peer_socket_native_or_null
704 * The native socket to close. No-op (not an error) if it is `.null()`.
705 * If not `.null()`, `peer_socket_native_or_null.m_native_handle` must be suitable for
706 * `Peer_socket::native_handle()`. In practice the use case informing release_native_peer_socket()
707 * is: `peer_socket_native = p.release()`, where `p` is a `Peer_socket`. Update:
708 * Another use case came about: receiving `peer_socket_native` over a `Native_socket_stream`.
709 */
710void release_native_peer_socket(Native_handle&& peer_socket_native_or_null);
711
712} // 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.