Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
client_context.hpp
Go to the documentation of this file.
1/* Flow-IPC: Shared Memory
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
23#include "ipc/session/app.hpp"
24#include "ipc/common.hpp"
25#include <flow/log/log.hpp>
26#include <boost/make_shared.hpp>
27#include <boost/shared_ptr.hpp>
28#include <boost/thread/future.hpp>
29#include <optional>
30#include <algorithm>
31
33{
34
35// Types.
36
37/**
38 * An object of this type facilitates the establishment, in client style, of a zero-copy-enabled capnp-RPC
39 * conversation with an opposing process's Context_server. On successful connect, it represents one such
40 * conversation until its destruction; create another `*this` to begin another such conversation.
41 *
42 * @see ipc::transport::struc::shm::rpc namespace doc header for an overview of Flow-IPC capnp-RPC support, including
43 * how the present class relates to the others there.
44 *
45 * Before a successful connection (sync_connect()) a `*this` is said to be in NULL state. After it and until
46 * destruction it is said to be in PEER state.
47 *
48 * Once a `*this` is in a PEER state (on successful sync_connect()), its capabilities/semantics/API are exactly
49 * identical to those of Server_context, objects of which type are generated by an opposing process's Context_server
50 * object. Thus the following relationships are analogous (if you're not familiar with Flow-IPC at large you may
51 * disregard):
52 * - In zero-copy-enabled-RPC land:
53 * Client_context (in PEER state) - Context_server - Server_context (always in PEER state).
54 * - In general Flow-IPC land:
55 * session::Client_session (in PEER state) - session::Session_server - session::Server_session (in PEER state).
56 *
57 * ### Background ###
58 * (This description will go down easier if you are familiar with capnp-RPC basics.)
59 *
60 * As introduced in ipc::transport::struc::shm::rpc namespace doc header, Flow-IPC's support of zero-copy RPC
61 * centers on its implementation of the `capnp::VatNetwork` interface: Session_vat_network. Consider its
62 * non-zero-copy cousin shipped with capnp, `TwoPartyVatNetwork`. To use it -- in the absence of simplifying
63 * all-in-one helpers like `capnp::EzRpcClient` -- the following essential steps are usually involved,
64 * on the *client end*:
65 * -# Connect a low-level transport stream (for local IPC, a Unix domain socket stream) to a listening server
66 * at some address (in Linux typically an abstract address; or more generally a file-addressed socket).
67 * -# Create a `TwoPartyVatNetwork`, giving it the stream endpoint (e.g., as a socket handle; from step 1)
68 * which it owns from that point on (up to and including closing it).
69 * -# Create an `RpcSystem`, passing it a reference to the `VatNetwork` from step 2; then perform RPC
70 * in various ways. (Typically, if one connected as a client, they would also obtain an interface impl
71 * from the other side and go from there; but this can be reversed... various possibilities exist.)
72 * -# Once either this side decides to be done, or the other side closes the conversation (the latter might
73 * be detected via a `Promise` returned by `TwoPartyVatNetwork::onDisconnect()`):
74 * - Deinit in the opposite order: `RpcSystem`, `VatNetwork` (which now owns the stream).
75 *
76 * ### How to use Client_context ###
77 * A Client_context object facilitates a very similar (almost identical) workflow. The following points apply:
78 * - To specify the target opposing server, use the ipc::session::App system. Don't worry: it is straightforward,
79 * yet it side-steps certain problems of portability/lack of abstraction and safety present when working with
80 * native IPC OS primitives directly. In any case you'll setup a session::Client_app to ID this application; and
81 * session::Server_app to ID the opposing application. (You'll give the same to Context_server on the
82 * opposing side.)
83 * - Client_context::sync_connect() will perform both steps 1 and 2 above...
84 * - ...except the `VatNetwork` -- which you should obtain via the vat_network() accessor of `*this` --
85 * is Session_vat_network. That is: it will avoid all copying of underlying messages, meaning they can
86 * effectively be of any size at all with no difference in performance/latency. Internally it will use
87 * SHared Memory (SHM) supplied by Flow-IPC.
88 * - You specify the SHM-provider to be used (behind the scenes!) via your choice of template parameter
89 * `Client_session_t`. As of this writing the following values are available out of the box.
90 * - (SHM-classic) session::shm::classic::rpc::Client_session. A good default choice.
91 * - (SHM-jemalloc) session::shm::arena_lend::jemalloc::rpc::Client_session. For more advanced considerations
92 * particularly in terms of safety (e.g., each side holds its own segregated SHM arena, and the other side
93 * may -- at a kernel level -- only read from it). There are also perf implications (the commercial-grade
94 * memory-manager, jemalloc, is used for individual allocations).
95 * - Please see the guided Manual and/or in-code doc headers such as for namespace ipc::shm. 99.9999% of this
96 * information will have no bearing on your use of capnp-RPC, semantically, but it may eventually be
97 * significant in terms of perf and safety considerations in production code.
98 *
99 * The `-> stream` feature (https://capnproto.org/news/#multi-stream-flow-control) has a potential impact on
100 * shared RAM (SHM) use, and possibly on performance. The way the related *flow control window* is determined
101 * differs from vanilla (non-zero-copy) capnp-RPC's impl (as described in that link).
102 * - You may control and/or observe the relevant window knob via `this->vat_network()->streaming_flow_window_ki()`.
103 * See Session_vat_network::streaming_flow_window_ki() for further info.
104 *
105 * ### For Flow-IPC-savvy users / Direct SHM use ###
106 * The above was written with a slant toward those who are familiar with capnp-RPC but may know next to nothing
107 * about Flow-IPC at large. However Client_context does provide access to the rest of Flow-IPC by exposing
108 * most (not quite all) features of ipc::session::Session. The latter is a (the!) gateway to the rest
109 * of Flow-IPC which is full-featured. To wit:
110 * - You may obtain the Flow-IPC `Session` object, after a successful sync_connect(). Call it `S`.
111 * - `S->session_shm()` and `S->app_shm()` give you direct access to SHM arenas, where you can directly
112 * construct C++ native objects (including arbitrary STL-compliant containers-of-containers-of-...),
113 * then transmit them to the opposing process and more. (See ipc::shm::stl namespace doc header.)
114 * - As you can see in other docs, `S->lend_object()` and `S->borrow_object()` enable sharing native SHM-stored
115 * objects between processes. To transmit the handle blobs returned by `->lend_object()`, you can use
116 * your shiny capnp-RPC session and/or any additional `Channel`s you opened via sync_connect()
117 * (or any other technique for that matter... but why would you need even more?!).
118 * - You may open direct-use channels (ipc::transport::Channel) via the optional args to sync_connect().
119 * These will be available immediately. You can then use them directly (for low-level transport work),
120 * upgrade them to ipc::transport::struc::Channel for a simplified capnp-message exchange, and so on.
121 * See the relevant documentation.
122 * - By choosing the template parameter (which has a sensible default value also) for
123 * `"ipc::session::shm::*::Client_session"` (in setting our template-arg `Client_session_t`), you may make
124 * use of several transport mechanisms including (as of this writing) POSIX MQs, boost.interprocess (in-SHM)
125 * MQs, and socket-streams (of bytes and/or native-handles/FDs).
126 *
127 * Some features of ipc::session::Session are not usable through a Client_context.
128 * - The capnp-metadata exchange (ipc::session::Session::mdt_builder() et al) at session-open is not available.
129 * This would have complicated the API, and we figure the capnp-RPC layer is more than sufficient for any
130 * negotiation or information exchange you could possibly desire!
131 * - While you can call `S->open_channel()` (which would open a new `Channel` *after* the session is connected),
132 * it will not work, as there is no way to specify a passive-channel-open handler. This would have, again,
133 * complicated the API -- and we'd want to perhaps make it available in a KJ-style way (promises, promises).
134 * - This is a possibility for future work. However (1) capnp-RPC is available, even for native-handle exchange
135 * if desired ("capability passing" -- see capnp-RPC's `getFd()`); and (2) even for vanilla Flow-IPC work
136 * most prefer to use the at-session-open channels (which are available in Client_context), so open_channel()
137 * et al are in the first place an advanced feature.
138 *
139 * @internal
140 *
141 * @todo There is a tiny protocol involved between rpc::Client_context and rpc::Context_server, but (internally)
142 * we lack a util::Protocol_negotiator; arguably we should add it, lest there is compatibility issue should we need
143 * to modify this protocol. The protocol is simple: the agreement that the first client-requested init-channel
144 * is actually for capnp-RPC purposes and not to be emitted to the `sync_connect()`/`async_accept()` invoker.
145 * Instead the channel is for this: Client sends one native handle (half of a socket-pair used for the capnp-RPC
146 * conversation by the `Session_vat_network`) over it. Having received it the server closes the channel. On
147 * the client side, here, `Client_context` retains its side of the channel indefinitely (reason: see
148 * sync_connect_impl() body; but basically because it does not know when opposing server has actually consumed
149 * the socket-pair). / Without `Protocol_negotiator`, which admittedly would complicate the impl annoyingly,
150 * needing to change this protocol in the future (such as to avoid keeping the aforementioned channel open
151 * indefinitely) is not a very civilized proposition. To deal with this: Just in case, for now, we've thrown in
152 * a reserved value along with the native handle. Alternatively one can always increment an
153 * ipc::session `Protocol_negotiator` version (both `local_(min|max)_proto_ver` <=> no compatibility with older
154 * protocol), if a change to this subsequent RPC-establishment protocol is required. After all both this module and
155 * ipc::session are parts of Flow-IPC.
156 *
157 * @endinternal
158 *
159 * @tparam Client_session_t
160 * A concrete type a-la session::Client_session but SHM-enabled. As of this writing
161 * out of the box the available types are listed above under: How to use Client_context.
162 * Naturally, the opposing Context_server must be parameterized in a compatible fashion.
163 * (E.g., `Client_context<ipc::session::shm::classic::Client_session<...knobs...>>` <=>
164 * `Context_server<ipc::session::shm::classic::Session_server<...same-knobs...>>`.)
165 */
166template<typename Client_session_t>
168 public flow::log::Log_context,
169 private boost::noncopyable
170{
171public:
172 // Types.
173
174 /// Convenience alias for template arg.
175 using Session_obj = Client_session_t;
176 static_assert(Session_obj::S_SHM_ENABLED,
177 "Client_context facilitates the use of Flow-IPC zero-copy (SHM) facilities for "
178 "zero-copy performance in using capnp-RPC; therefore the supplied Client_session_t template "
179 "parameter must be a SHM-enabled Client_session variant, session::shm::*::Client_session*.");
180 static_assert(Session_obj::Channel_obj::S_HAS_NATIVE_HANDLE_PIPE,
181 "Client_context, per its protocol with the opposing Context_server, sends a native handle -- "
182 "the stream-socket underlying the capnp-RPC conversation -- over the first init-channel; "
183 "therefore the supplied Client_session_t must be configured to emit Native_handle-bearing "
184 "channels (e.g., see aliases in session::shm::*::rpc namespaces).");
185
186 /**
187 * The Session_vat_network concrete type -- and `capnp::VatNetwork` interface impl -- that we produce.
188 * See Session_vat_network docs; but in short, generally, once constructed this guy is used ~identically
189 * to `capnp::TwoPartyVatNetwork`.
190 */
191 using Vat_network = typename Session_obj::Vat_network;
192
193 // Constructors/destructor.
194
195 /**
196 * Constructs us without establishing the capnp-RPC session. Use sync_connect() to establish it.
197 *
198 * @warning `cli_app_ref` and `srv_app_ref` must remain alive past this ctor: their *addresses*
199 * are stored and accessed at various points later. As usual, unless null, same for `*logger_ptr`.
200 * **The required lifetime of these objects may exceed that of the constructed `*this`.**
201 * Therefore:
202 * @see Please read Session_vat_network doc header section "Object lifetimes." It explains how/why and more
203 * importantly how long these objects must stay alive. Promise: it's pretty easy. (Hard part is to not
204 * assume they just need to outlive `*this`. The design of capnp-RPC/KJ is the reason for this.)
205 * @note Spoiler alert: An easy approach is to keep `cli/srv_app_ref` and `*logger_ptr` objects alive past
206 * `*kj_io`.
207 *
208 * @param logger_ptr
209 * Logger to use for logging subsequently. (You may use null to forego this completely.)
210 * See lifetime warning above.
211 * @param kj_io
212 * A KJ event loop context.
213 * @param cli_app_ref
214 * Properties of this client application. See lifetime warning above.
215 * @param srv_app_ref
216 * Properties of the opposing server application. See lifetime warning above.
217 * @param enable_hndl_transport
218 * `true` means native handles (a/k/a capabilities in capnp-RPC parlance) can
219 * be RPC-transmitted (i.e., your interface impls and clients can use `.getFd()` and yield something
220 * as opposed to nothing). `false` means the opposite; in particular any native-handles arriving via
221 * capnp-RPC (as enabled via the #Vat_network returned by vat_network()) shall be disregarded.
222 * If you do not use `.getFd()` feature of the capnp-RPC system, it is best to set this to `false`,
223 * it is said, for safety and possibly even security.
224 */
225 explicit Client_context(flow::log::Logger* logger_ptr, kj::AsyncIoContext* kj_io,
226 const session::Client_app& cli_app_ref, const session::Server_app& srv_app_ref,
227 bool enable_hndl_transport = true);
228
229 /**
230 * Destructor; in particular (assuming sync_connect() succeeded) shuts down the #Vat_network and the
231 * #Session_obj, in that order. You must destroy any related #Rpc_system before invoking this dtor.
232 * If you have any directly-obtained (via `->construct<T>()` or `->borrow_object<T>()`) SHM-handles from
233 * `*this`, you must have nullified them before invoking this dtor.
234 *
235 * (Internally the #Session_obj may outlive this dtor briefly -- until the KJ event loop has disposed of any
236 * message capnp-RPC has left in it -- but by then it is nobody's concern but its own. See ctor doc header.)
237 *
238 * Informally we recommend, also, that once you disconnect your capnp-RPC conversation, or
239 * the other side does so -- this can be detected via `vat_network()->on_disconnect()`-returned `kj::Promise` --
240 * you destroy `*this` sans delay. (Do not worry: there is no race condition. But we digress.)
241 */
243
244 // Methods.
245
246 /**
247 * To be invoked successfully at most once, it synchronously and non-blockingly attempts to
248 * connect to an opposing `Context_server`; and synchronously reports failure or success, the latter
249 * showing `*this` has entered PEER state. Failure means `*this` remains in NULL state.
250 *
251 * Note that `*this` that has entered PEER state can never change state subsequently
252 * (even on transmission error); once a PEER, always a PEER.
253 *
254 * Essentially this convenience operation has one main aspect:
255 * - It bundles together the initiation of a Flow-IPC session::Session and the creation of a `capnp::VatNetwork`
256 * (similar to a vanilla `capnp::TwoPartyVatNetwork` but zero-copy-enabled using Flow-IPC SHM resources from
257 * said `Session`). So: one immediate op; and then you can make an `RpcSystem` and go capnp-RPC-ing happily; and
258 * (optionally) make use of certain Flow-IPC `Session` features as well (see below and class doc header).
259 *
260 * @note Upon entering PEER state (i.e., on success of sync_connect()): knobs of interest are available
261 * through vat_network() method which gives access to the core Session_vat_network.
262 *
263 * ### How does it know "where" the Session_server is listening? / Why non-blocking and synchronous? ###
264 * Please see session::Client_session::sync_connect() doc header's similar section. It is best to be aware
265 * of this as background.
266 *
267 * ### Error conditions ###
268 * If invoked already in PEER state, this returns `false` and otherwise does nothing. The following assumes
269 * otherwise (that it was invoked properly in NULL state). Then:
270 *
271 * On success returns `true`. On connect fail, throws an exception:
272 * - `flow::error::Runtime_error` (which is an `std::runtime_error` which is an `std::exception`)
273 * if it's from originating in Flow-IPC proper;
274 * - `kj::Exception` if from capnp internals;
275 * - generic `std::exception` and `...` might also be possible.
276 *
277 * In case of `Runtime_error`: The `Error_code` may be accessed via `.code()` (also `.code().message()`,
278 * `.what()`) of the exception object. #Error_code generated:
279 * - interprocess-mutex-related errors (probably from boost.interprocess) w/r/t reading the CNS (PID file),
280 * file-related system errors w/r/t reading the CNS (PID file) (see Session_server doc header for background),
281 * - error::Code::S_CLIENT_NAMESPACE_STORE_BAD_FORMAT (bad CNS contents),
282 * those emitted by transport::Native_socket_stream::sync_connect(),
283 * those emitted by transport::struc::Channel::send(),
284 * those emitted by transport::struc::Channel via on-error handler (most likely
285 * transport::error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE indicating graceful shutdown of opposing process
286 * coincidentally during log-in procedure, prematurely ending session while it was starting),
287 * error::Code::S_CLIENT_MASTER_LOG_IN_RESPONSE_BAD,
288 * error::Code::S_INVALID_ARGUMENT (other side expected other sync_connect() overload with
289 * non-null `init_channels_by_srv_req` arg),
290 * session::error::Code::S_SESSION_OPEN_CHANNEL_SERVER_CANNOT_PROCEED_RESOURCE_UNAVAILABLE (SHM-jemalloc
291 * internal-use socket-connection could not be obtained from OS by opposing side),
292 * - possibly others.
293 *
294 * You may retry `this->sync_connect()`, if an exception is thrown. (If an exception is thrown, post-conditions:
295 * `*this` remains in NULL state; (if applicable) `*init_channels_by_srv_req` is empty;
296 * (if applicable) `*init_channels_by_cli_req_pre_sized` is filled with NULL-state objects, container size
297 * unchanged.)
298 *
299 * @note To comport with capnp-RPC/KJ style code flow, this API does not have a standard Flow-style
300 * optional `Error_code*` out-arg through which to communicate success/errors instead of exceptions if desired;
301 * it shall always fire an exception on error, otherwise return `true`.
302 *
303 * ### Seriously, why no `async_connect()`? Isn't it common to perform an async socket-connect operation? ###
304 * In a local-IPC context: In short, actually, no: there is no waiting involved. (Even in a networked context
305 * the potential asynchronicity -- waiting -- really comes from the fact that connection establishment with
306 * a *listening* server involves networked round trips which are not instant; in our local case it is instant.)
307 *
308 * There is however a to-do elsewhere (which would propagate here as well) for a relatively exotic
309 * wait-until-server-pops-up operation.
310 *
311 * @param init_channels_by_cli_req_pre_sized
312 * Null or pointer to container of `Session_obj::Channel_obj` with `.size()` specifying how many channels
313 * this side is requesting to be opened on its behalf; each element will be move-assigned
314 * a PEER-state `Channel_obj` on success. Recommend simply ct-ing with `(n)` or `.resize(n)` which
315 * loads it with default-cted (NULL-state) objects to be replaced. null is treated same as
316 * `.empty()`.
317 * @param init_channels_by_srv_req
318 * Null or pointer to container of `Channel_obj` which shall be `.clear()`ed and replaced
319 * by a container of PEER-state `Channel_obj` on success the number being specified by the opposing
320 * (server) side. The number may be zero. null is allowed if and only if the number is zero;
321 * otherwise error::Code::S_INVALID_ARGUMENT is emitted.
322 * @return `true` on successful connect; `false` if invoked in PEER state (already connected).
323 * Exception thrown on failure to connect (`.code()` indicating why).
324 */
325 bool sync_connect(typename Session_obj::Channels* init_channels_by_cli_req_pre_sized = nullptr,
326 typename Session_obj::Channels* init_channels_by_srv_req = nullptr);
327
328 /**
329 * Identical to sync_connect() with same signature but disables zero-copy transport.
330 * That is underlying messages are sent in the mode compatible with vanilla capnp-RPC.
331 * This requires some knobs to be set appropriately at the opposing process. To wit:
332 * - If using Ez_rpc_server: use ctor that takes `transport_method_func` arg; it must return `false`,
333 * if the session::Client_app arg equals the `cli_app_ref` passed to `*this` ctor.
334 * - If using Context_server: use Context_server::accept() that takes `transport_method_func` arg;
335 * it must (...see previous bullet...).
336 * - If using Session_vat_network: use the "special mode" ctor arg (see Session_vat_network ctor doc headers).
337 * - Or use `capnp::VatNetwork` or `capnp::EzRpcServer`.
338 *
339 * @param init_channels_by_cli_req_pre_sized
340 * See sync_connect().
341 * @param init_channels_by_srv_req
342 * See sync_connect().
343 * @return See sync_connect().
344 */
345 bool sync_connect_sans_shm_transport(typename Session_obj::Channels* init_channels_by_cli_req_pre_sized = nullptr,
346 typename Session_obj::Channels* init_channels_by_srv_req = nullptr);
347
348 /**
349 * Returns pointer to Session_vat_network established for use in your rpc::Rpc_system; or null if
350 * no sync_connect() has succeeded or been invoked at all. The #Vat_network is valid if and only if
351 * `*this` exists.
352 *
353 * @see `vat_network()->streaming_flow_window_ki()` is a perf-knob of potential interest accessible through here.
354 * See Session_vat_network::streaming_flow_window_ki() doc header(s).
355 *
356 * See Session_vat_network docs; but in short, generally, once constructed this guy is used ~identically
357 * to `capnp::TwoPartyVatNetwork`.
358 *
359 * @return See above.
360 */
362
363 /**
364 * Immutable counterpart to the other overload.
365 * @return See above.
366 */
367 const Vat_network* vat_network() const;
368
369 /**
370 * Returns pointer to #Session_obj, if sync_connect() has succeeded; or null if no sync_connect() has succeeded
371 * or been invoked at all. The #Session_obj is valid if and only if `*this` exists.
372 *
373 * @see class doc header for overview of which features accessible through this #Session_obj are available
374 * (and which are not).
375 *
376 * @return See above.
377 */
379
380 /**
381 * Immutable counterpart to the other overload.
382 * @return See above.
383 */
384 const Session_obj* session() const;
385
386private:
387 // Types.
388
389 /// Basically `shared_ptr<Session_obj>`.
390 using Session_ptr = typename Vat_network::Shm_lender_borrower_ptr;
391
392 // Methods.
393
394 /**
395 * Session-hosed handler for #m_session. This only logs as opposed to reporting to the user of `*this` for
396 * reasons explained inside (this is of some importance). May run after the relevant `*this` dies; hence
397 * the needed bits of info are to be captured and passed-in as args.
398 *
399 * @param logger_ptr
400 * See ctor.
401 * @param cli_app
402 * See ctor.
403 * @param srv_app
404 * See ctor.
405 * @param err_code
406 * From error handler.
407 */
408 static void on_session_hosed(flow::log::Logger* logger_ptr,
409 const session::Client_app& cli_app, const session::Server_app& srv_app,
410 const Error_code& err_code);
411
412 /**
413 * Impl of sync_connect() and sync_connect_sans_shm_transport().
414 *
415 * @param init_channels_by_cli_req_pre_sized
416 * See `sync_connect*()`.
417 * @param init_channels_by_srv_req
418 * See `sync_connect*()`.
419 * @param sans_shm_transport
420 * Which method are we impl-ing?
421 * @return See `sync_connect*()`.
422 */
423 bool sync_connect_impl(typename Session_obj::Channels* init_channels_by_cli_req_pre_sized,
424 typename Session_obj::Channels* init_channels_by_srv_req,
425 bool sans_shm_transport);
426
427 // Data.
428
429 /// KJ event loop context.
430 kj::AsyncIoContext* const m_kj_io;
431
432 /// See ctor. Stored in case we must re-initialize #m_session, if sync_connect() only partially succeeds.
434 /// See ctor. Stored in case we must re-initialize #m_session, if sync_connect() only partially succeeds.
436
437 /// See ctor.
439
440 /**
441 * Session in NULL state (until sync_connect() succeeds) or PEER state (subsequently).
442 *
443 * Held via shared handle, as #m_network requires. Corollary: `*m_session` may outlive `*this`.
444 * See Session_vat_network doc header, "Object lifetimes." That is also why we warn about `Logger` and `App`
445 * lifetimes in our ctor doc header.
446 */
448
449 /**
450 * Null until sync_connect() succeeds; then the idle channel being kept up, having been used
451 * to convey the capnp-RPC transport handle to the opposing Context_server. So: synchronously
452 * in a successful sync_connect_impl(): grab the first init-channel (do not emit to user); establish
453 * a socket-pair connection for capnp-RPC; sync-send half of that pair; and then just keep the
454 * `Channel` -- still open, or at least not closed on our account -- around until destruction.
455 *
456 * ### Rationale: Why keep it open? ###
457 * See sync_connect_impl() body for details. In short: When we destroy the `Channel` (<=> close our-side
458 * endpoint), the Context_server may well happen to not have quite gotten to consume the native-handle off
459 * the us-to-them half-pipe yet. It's a bidirectional pipe, and certain technicalities mean that
460 * after our-side closing happens, their-side receiving won't succeed; so instead we simply keep it open:
461 * they can read it whenever. The cost is mainly the extra endpoint resource (in practice, a socket).
462 * (It *is* possible to make it work, essentially by handling the channel with full minimalism as entirely
463 * unidirectional. Then no need for #m_rpc_setup_channel. Again, though, for all that see sync_connect_impl().)
464 */
465 std::optional<typename Session_obj::Channel_obj::Async_io_obj> m_rpc_setup_channel;
466
467 /// Null (until sync_connect() succeeds) or the established #Vat_network (subsequently).
468 std::optional<Vat_network> m_network;
469}; // class Client_context
470
471// Template implementations.
472
473template<typename Client_session_t>
475 kj::AsyncIoContext* kj_io,
476 const session::Client_app& cli_app_ref,
477 const session::Server_app& srv_app_ref,
478 bool enable_hndl_transport) :
479 flow::log::Log_context(logger_ptr, Log_component::S_RPC),
480 m_kj_io(kj_io),
481 m_cli_app_ref(cli_app_ref),
482 m_srv_app_ref(srv_app_ref),
483 m_enable_hndl_transport(enable_hndl_transport),
484 m_session(boost::make_shared<Session_obj>(get_logger(), m_cli_app_ref, m_srv_app_ref,
485 [logger_ptr, &cli_app_ref, &srv_app_ref](const Error_code& err_code)
486 { on_session_hosed(logger_ptr, cli_app_ref, srv_app_ref, err_code); }))
487{
488 FLOW_LOG_INFO("rpc::Client_ctx [" << *this << "]: "
489 "Created in NULL state (inactive). Inactive ipc::session::Session [" << *m_session << "] exists.");
490}
491
492template<typename Client_session_t>
494{
495 FLOW_LOG_INFO("rpc::Client_ctx [" << *this << "]: "
496 "Shutting down. Session_vat_network shall shut down; then the ipc::session::Session "
497 "(unless a straggling capnp-RPC message keeps the latter alive a bit longer).");
498}
499
500template<typename Client_session_t>
501void Client_context<Client_session_t>::on_session_hosed(flow::log::Logger* logger_ptr,
502 const session::Client_app& cli_app,
503 const session::Server_app& srv_app,
504 const Error_code& err_code) // Static.
505{
506 FLOW_LOG_SET_CONTEXT(logger_ptr, Log_component::S_RPC);
507 FLOW_LOG_INFO("rpc::Client_ctx-created ipc::session::Session [" << cli_app.m_name << "->" << srv_app.m_name << "]: "
508 "session-hosed handler fired (code [" << err_code << "] [" << err_code.message() << "]). "
509 "This is likely normal, as the opposing process decided to end session; the RPC-system will "
510 "have detected same, and user RPC session should be ended or ending imminently, at which point "
511 "proper user code shall shut-down this Client_ctx which will shut down the Session_vat_network and "
512 "lastly the ipc::session::Session.");
513
514 /* Why do we merely log but in no way report this to `*this` user?
515 * (After all with a vanilla Flow-IPC session we make a big deal of the session-hosed handler one must pass
516 * to its ctor.) Answer: The usefulness of the handler is, outside of emitting a reason error-code (which
517 * actually is not *that* interesting typically; simply the other side decided to end the session), to *signal*
518 * to the user that they cannot carry on. Moreover, excepting crashes and the like, this is not a catastrophe:
519 * it means the user *should* destroy the session and everything related to it, ideally soon; but behavior
520 * remains not ill-defined until then. (Details omitted; in short for some SHM-types this requires that each
521 * of two opposing destructors waits until the other begins executing, before it can execute; for others this is not
522 * necessary.)
523 *
524 * With a general session, users may generate `Channel`s which are independent of the session in terms of their
525 * lifetimes -- even though informally it is recommended to follow the best practice of init/de-init order
526 * (A up, B up, B down, A down), it is not required. So formally speaking it makes sense to have a session-end
527 * signal separately from channel-end signal(s). In our case, however, we have a *mandatory* capnp-RPC session
528 * we start; really it's the main point of a `*this` after all; and *that* system already provides a natural
529 * signal as to the other side being done -- which is customary for capnp-RPC users to use. That can be something
530 * in their own protocol, or they can use vat_network()->on_disconnect() promise. We indicate this explicitly
531 * in the dtor doc header for instance.
532 *
533 * Therefore supplying a signal through the Flow-IPC session as well is an unnecessary complication of the API --
534 * an asynchronous one at that which is particularly annoying. It becomes ambiguous as to how to handle
535 * disconnection. So let's not make it hard. */
536} // Client_context::on_session_hosed()
537
538template<typename Client_session_t>
540 (typename Session_obj::Channels* init_channels_by_cli_req_pre_sized,
541 typename Session_obj::Channels* init_channels_by_srv_req)
542{
543 return sync_connect_impl(init_channels_by_cli_req_pre_sized, init_channels_by_srv_req, false);
544}
545
546template<typename Client_session_t>
548 (typename Session_obj::Channels* init_channels_by_cli_req_pre_sized,
549 typename Session_obj::Channels* init_channels_by_srv_req)
550{
551 return sync_connect_impl(init_channels_by_cli_req_pre_sized, init_channels_by_srv_req, true);
552}
553
554template<typename Client_session_t>
556 (typename Session_obj::Channels* init_channels_by_cli_req_pre_sized,
557 typename Session_obj::Channels* init_channels_by_srv_req,
558 bool sans_shm_transport)
559{
560 using Channels = typename Session_obj::Channels;
561 using util::Blob_const;
562 namespace asio_local = asio_local_stream_socket::local_ns;
563 using asio_local::connect_pair;
565 using flow::util::Task_engine;
566 using flow::error::Runtime_error;
567 using boost::promise;
568 using std::string;
569 using std::exception;
570
571 if (m_network)
572 {
573 FLOW_LOG_WARNING("rpc::Client_ctx [" << *this << "]: capnp-RPC connect requested, but we are already connected. "
574 "Bug? Ignoring.");
575 return false;
576 }
577 // else
578
579 /* We request 1 channel for capnp-RPC; plus any they might want for their own purposes.
580 * Naturally the Server_context knows this agreement.
581 * (See @todo in class doc header regarding protocol negotiation though.) */
582 Channels actual_init_channels_by_cli{1 + (init_channels_by_cli_req_pre_sized
583 ? init_channels_by_cli_req_pre_sized->size()
584 : 0)};
585 FLOW_LOG_INFO("rpc::Client_ctx [" << *this << "]: capnp-RPC connect requested. Synchronous, non-blocking "
586 "ipc::session::Session connect initiating; on success will hook up the "
587 "[zero-copy-enabled? = [" << (!sans_shm_transport) << "]] Session_vat_network.");
588
589 Native_handle local_hndl;
590 Native_handle remote_hndl;
591 try
592 {
593#ifndef NDEBUG
594 const bool ok =
595#endif
596 m_session->sync_connect(m_session->mdt_builder(), &actual_init_channels_by_cli, nullptr,
597 init_channels_by_srv_req);
598 assert(ok && "We had a NULL-state Client_session and tried .sync_connect(); why would it have reported otherwise?");
599
600 /* .sync_connect() would have thrown on error (as advertised) (probably other guy isn't up; m_network remained
601 * null). Otherwise we carry on: */
602
603 if (init_channels_by_cli_req_pre_sized && (!init_channels_by_cli_req_pre_sized->empty()))
604 {
605 assert(actual_init_channels_by_cli.size() > 1);
606 std::move(++actual_init_channels_by_cli.begin(), actual_init_channels_by_cli.end(),
607 init_channels_by_cli_req_pre_sized->begin());
608 actual_init_channels_by_cli.resize(1);
609 }
610 assert(actual_init_channels_by_cli.size() == 1);
611 auto& rpc_setup_channel = actual_init_channels_by_cli.front();
612
613 /* Per our protocol with the opposing Context_server (see also the class doc header @todo regarding
614 * Protocol_negotiator), rpc_setup_channel exists for one purpose: to convey -- from us to them -- a single
615 * native handle, namely one end of a pre-connected stream-socket pair, over which the two Session_vat_network
616 * peer objects (ours and theirs) shall subsequently transmit their (small) messages. So: create said
617 * socket pair; send one end over the channel and flush it (the channel's work is then done); and feed
618 * the other end to our Session_vat_network.
619 *
620 * Why not just have Session_vat_network use rpc_setup_channel's own low-level transport (skipping this
621 * dance)? Answer: KJ/capnp async-I/O machinery (kj::AsyncIoStream et al) requires a stream-type
622 * socket; whereas the channel's own transport is not, in general, that. (As of this writing it is typically a
623 * SOCK_SEQPACKET-type Unix-domain socket; depends on compile-time Native_socket_stream_cfg settings; but that
624 * detail is not the point really: The point is we want it to work regardless of how Native_socket_stream might
625 * itself be internally implemented/how it may or may not expose related resources to the world. Cf.: this Channel
626 * type can, generically, transmit `Native_handle`s, end of; so we use it for its purpose (one of them).)
627 *
628 * Compared to many other places in session-land of Flow-IPC, wherein typically the server-designated side
629 * creates resources/client consumes them, here we do the reverse: we connect_pair(); they (briefly)
630 * await one of the results of that. Why? Answer: Then we can keep everything synchronous -- no KJ async/promise
631 * dance (we just sync_connect(), send_native_handle(), and *end_sending(); the latter is ~instant
632 * in practice, so it's in effect synchronous too). They already need to do the KJ async/promise dance
633 * on account of the Session::async_accept() step; so the async_receive...() is tacked on to that. */
634
635 {
636 Task_engine task_engine; // Purely a formality required by the Peer_socket (boost.asio) API.
637 Peer_socket local_sock{task_engine};
638 Peer_socket remote_sock{task_engine};
639 Error_code sys_err_code;
640 connect_pair(local_sock, remote_sock, sys_err_code);
641 /* (Could've used the throwing version of that API, but our message-and-such is arguably nicer. Plus
642 * technically we promised to throw Runtime_error, if the problem is within Flow-IPC proper which this
643 * arguably is.) */
644 if (sys_err_code)
645 {
646 throw Runtime_error{sys_err_code, "Client_context::sync_connect_impl(): connect_pair()"};
647 }
648 // else
649 local_hndl = Native_handle{local_sock.release()};
650 remote_hndl = Native_handle{remote_sock.release()};
651 }
652
653 {
654 /* Send via the async-I/O API: less fuss than servicing the sync_io-pattern API for this one-off op.
655 * Note async_io_obj() subsumes the channel's transport (and the m_rpc_setup_channel dtor shall close it). */
656 m_rpc_setup_channel.emplace(rpc_setup_channel.async_io_obj());
657
658 uint8_t protocol_dummy_payload = 0; // Reserved (see Protocol_negotiator to-do in class doc header).
659 m_rpc_setup_channel->hndl_snd()->send_native_handle(remote_hndl,
660 Blob_const{&protocol_dummy_payload,
661 sizeof(protocol_dummy_payload)});
662 // (That throws Runtime_error on error, due to null Error_code* arg.)
663
664 /* Send the graceful-close after that. Neither the above nor this will internally hit would-block
665 * (fresh channel), in any OS we know of at least, but that's irrelevant anyway: For reasons
666 * we're about to explain, we're keeping *m_rpc_setup_channel alive indefinitely anyway. So there's no
667 * need for the typical async_end_sending(F) (where F() signals us to proceed <=> anything pending has been sent)
668 * dance. */
669 m_rpc_setup_channel->hndl_snd()->end_sending();
670
671 /* Sent: the other side has (or on its schedule will have) its own copy of the handle;
672 * our copy has done its job; close it (descriptor/handle goes away; description/kernel endpoint lives). */
673 remote_hndl.close();
674
675 /* The channel endpoint (reminder: of the channel we used to transmit remote_hndl, which is half of
676 * capnp-RPC transmission-pipe socket-pair) itself, however, we deliberately keep around
677 * in *m_rpc_setup_channel until *this dies. Why though?
678 *
679 * To answer: First consider the minimal, most-elegant algorithm in purely algorithmic and resource-use
680 * terms. In that one, we have a Unix-domain socket S, and Context_server has socket D. We send the
681 * payload (remote_hndl basically), an EOF token; and now no longer need S. Opposing server-side: we grab
682 * D off the accept queue as soon as possible; async-read the payload; and now no longer need D.
683 *
684 * Can this be arranged in practice? Yes. The original init-channel, before .async_io_obj() conversion (which
685 * we would forego in this hypothetical), is a sync_io-pattern Channel, so its ->hndl_snd() essentially has S.
686 * To wrangle that, use it minimalistically: Client-side (here) do only start_send_...(); server-side only
687 * start_receive_...(). Then, very similarly to the above, send_native_handle(), end_sending() -- and then
688 * drop the init-channel object (close S); no m_rpc_setup_channel to store (no socket taken from OS
689 * indefinitely).
690 *
691 * @todo We can and arguably should do it -- *if* an extra per-session socket (FD) is bothersome
692 * in the field (seems very unlikely but conceivable). The difficulty, in practice, is server-side.
693 * Client-side in reality all the required ops are synchronous and will not require us to hook-up an
694 * async-wait functor (it can be a no-op). Server-side, though, there's the async-read which will mean
695 * start_receive_...(F) will need an F() that'll actually do the wait; and in Context_server that needs to
696 * be hooked to the KJ event-loop; hence an FdObserver or similar. The best way to do *that* would be
697 * to provide a utility module that links our sync_io-pattern API with KJ-style event loops -- itself
698 * an excellent to-do potentially useful for KJ users orthogonally to anything else. Until then it's too
699 * hairy to custom-write it just for Context_server, just to save a socket taken from the OS.
700 *
701 * @todo (The "to-do" mentioned in preceding to-do) Elsewhere develop a public utility that links
702 * KJ event loops to sync_io-pattern APIs, in spirit similarly to what we already provide for boost.asio
703 * event loops. (Make it available to users; and can also use in the preceding to-do ourselves, for the
704 * Context_server side.)
705 *
706 * OK! So then since doing the sync_io approach is, at best, a to-do: Why not just use an async-I/O
707 * Channel? Indeed that is what we do, as you can see; bringing us back to the original question:
708 * why keep our side around (as m_rpc_setup_channel)? Naively it would seem we're just doing the
709 * async-I/O-ish version of the S/D algorithm above, the only difference being the signalling
710 * from a Channel background thread ("thread W"), server-side only at that. So what can go wrong? Answer:
711 * the wrongness comes from a technicality: ->hndl_snd/rcv() is really Native_socket_stream, and
712 * although from our PoV server-side we are only doing receive-ops and no send-ops, internally an
713 * async-I/O Native_socket_stream automatically sets itself up for bidirectional use, including a one-time
714 * initial internal send of some Protocol_negotiator info. Now, consider this fully-possible (and observed)
715 * scenario:
716 *
717 * We send hndl, send EOF, close; then a split second later Context_server reaches the point where it can deal
718 * with the capnp-RPC setup (perhaps it was setting up SHM stuff for a bit up to that point, so it's slightly
719 * behind us). So Context_server side of this channel will *internally* **send** that initial protocol-version
720 * stuff -- but it can't; our side has closed for sending *and* (saliently) receiving. In Linux at least
721 * this is the broken-pipe error (EPIPE); but it makes sense generically/conceptually too. Bottom line is:
722 * transport::Native_socket_stream internally needs to write even when the user only needs to read; and
723 * our client-side endpoint-closing behavior breaks that part.
724 *
725 * So that's why we solve it by simply not closing our side. The to-dos above would tighten this up, but
726 * the costs are (1) an extra per-session socket taken from OS, (2) slight bit of added code, and (3)
727 * this endless comment. / Last thing: one can alternatively use a handshake protocol, wherein after
728 * the send hndl/send EOF step we'd await an ack; server conversely would async-read and then send ack.
729 * Upon receiving ack, or error, we can close our-side Channel. It would require some concurrency
730 * consideration though: perhaps a mostly-idle Single_thread_task_loop (our own thread W) for channel-closings
731 * only. Only a few more lines of code ultimately -- but it still adds *more* to the algorithm, whereas
732 * the solution in the to-dos above makes it simpler (conceptually). So: *if* there's anything to do at all
733 * the to-dos are probably the better approach. */
734 }
735
736 if (m_enable_hndl_transport)
737 {
738 // Let it use the sensible default for last arg.
739 m_network.emplace(get_logger(), m_kj_io,
740 sans_shm_transport ? Session_ptr{} : Session_ptr{m_session},
741 std::move(local_hndl));
742 }
743 else
744 {
745 // As requested disable handle-transmission (last arg).
746 m_network.emplace(get_logger(), m_kj_io,
747 sans_shm_transport ? Session_ptr{} : Session_ptr{m_session},
748 std::move(local_hndl), 0);
749 }
750 assert(local_hndl.null() && "Session_vat_network ctor promises to consume the transport handle immediately.");
751 /* (That *m_network construction might throw KJ or capnp error; we are not sure exactly all the specific
752 * possibilities of what would throw.) */
753 }
754 catch (...)
755 {
756 /* Get the message for logging, if we can. (This dispatcher fuss, as opposed to just catching
757 * `const exception&`, is because: kj::Exception -- possible from Session_vat_network ctor -- does not
758 * derive from std::exception. KJ in fact *throws* an internal sub-class deriving from both, so the
759 * middle arm below is only for a bare-thrown kj::Exception; can't hurt.) */
760 string msg;
761 try { throw; }
762 catch (const exception& exc) { msg = exc.what(); }
763 catch (const kj::Exception& exc) { msg = exc.getDescription().cStr(); }
764 catch (...) { msg = "(unknown exception type)"; }
765
766 FLOW_LOG_WARNING("rpc::Client_ctx [" << *this << "]: A step of capnp-RPC connect (the ipc::session::Session "
767 "[" << *m_session << "] connect itself; socket-pair creation; socket-handle transmission; "
768 "Session_vat_network ctor) threw exception [" << msg << "] which we shall re-throw; "
769 "getting `*this` and out-args back to pristine state first.");
770 /* Close whatever socket-pair handles remain in our custody (each of these no-ops on null handle;
771 * in particular Session_vat_network ctor -- if reached -- consumes (nullifies) local_hndl immediately;
772 * in any case we cannot double-close no matter when it threw). */
773 remote_hndl.close();
774 local_hndl.close();
775 m_rpc_setup_channel.reset(); // Get this back to pristine state too.
776
777 /* As advertised: Undo anything we might have placed into the init-channels container(s) upon successful
778 * sync_connect() if any. (We don't bother trying to bring back whatever random stuff they had
779 * in *init_channels_by_cli_req_pre_sized; placing NULL-state Channel{}s there is fine.) */
780 if (init_channels_by_cli_req_pre_sized)
781 {
782 *init_channels_by_cli_req_pre_sized = Channels{init_channels_by_cli_req_pre_sized->size()};
783 }
784 if (init_channels_by_srv_req)
785 {
786 init_channels_by_srv_req->clear();
787 }
788
789 /* Whether Session connect failed (leaving it in NULL state) or succeeded (PEER state) before the throw:
790 * replace it with a fresh NULL-state one; a subsequent retry starts from scratch. */
791 m_session
792 = boost::make_shared<Session_obj>(get_logger(), m_cli_app_ref, m_srv_app_ref,
793 [logger_ptr = get_logger(), &cli_app = m_cli_app_ref, &srv_app = m_srv_app_ref]
794 (const Error_code& err_code)
795 { on_session_hosed(logger_ptr, cli_app, srv_app, err_code); });
796 assert((!m_network) && "If Session_vat_network ctor (last step) threw -- m_network should still be null! Bug?");
797
798 throw;
799 }
800 // Got here: noice! (m_network ctor logged just fine, so let's not.)
801
802 return true;
803} // Client_context::sync_connect_impl()
804
805template<typename Client_session_t>
808{
809 return m_network ? &(*m_network) : nullptr;
810}
811
812template<typename Client_session_t>
815{
816 return const_cast<Client_context*>(this)->vat_network();
817}
818
819template<typename Client_session_t>
822{
823 return m_network ? m_session.get() : nullptr;
824}
825
826template<typename Client_session_t>
829{
830 return const_cast<Client_context*>(this)->session();
831}
832
833template<typename Client_session_t>
834std::ostream& operator<<(std::ostream& os, const Client_context<Client_session_t>& val)
835{
836 const auto network = val.vat_network();
837 os << '[';
838 if (network)
839 {
840 os << "session[" << *(val.session()) << "] vat_netwk[" << *network << ']';
841 }
842 else
843 {
844 os << "null";
845 }
846 return os << "]@" << &val;
847}
848
849} // namespace ipc::transport::struc::shm::rpc
An object of this type facilitates the establishment, in client style, of a zero-copy-enabled capnp-R...
Session_obj * session()
Returns pointer to Session_obj, if sync_connect() has succeeded; or null if no sync_connect() has suc...
const Session_obj * session() const
Immutable counterpart to the other overload.
bool sync_connect(typename Session_obj::Channels *init_channels_by_cli_req_pre_sized=nullptr, typename Session_obj::Channels *init_channels_by_srv_req=nullptr)
To be invoked successfully at most once, it synchronously and non-blockingly attempts to connect to a...
typename Vat_network::Shm_lender_borrower_ptr Session_ptr
Basically shared_ptr<Session_obj>.
Session_ptr m_session
Session in NULL state (until sync_connect() succeeds) or PEER state (subsequently).
bool sync_connect_sans_shm_transport(typename Session_obj::Channels *init_channels_by_cli_req_pre_sized=nullptr, typename Session_obj::Channels *init_channels_by_srv_req=nullptr)
Identical to sync_connect() with same signature but disables zero-copy transport.
const Vat_network * vat_network() const
Immutable counterpart to the other overload.
const session::Server_app & m_srv_app_ref
See ctor. Stored in case we must re-initialize m_session, if sync_connect() only partially succeeds.
std::optional< Vat_network > m_network
Null (until sync_connect() succeeds) or the established Vat_network (subsequently).
static void on_session_hosed(flow::log::Logger *logger_ptr, const session::Client_app &cli_app, const session::Server_app &srv_app, const Error_code &err_code)
Session-hosed handler for m_session.
Vat_network * vat_network()
Returns pointer to Session_vat_network established for use in your rpc::Rpc_system; or null if no syn...
Client_session_t Session_obj
Convenience alias for template arg.
typename Session_obj::Vat_network Vat_network
The Session_vat_network concrete type – and capnp::VatNetwork interface impl – that we produce.
std::optional< typename Session_obj::Channel_obj::Async_io_obj > m_rpc_setup_channel
Null until sync_connect() succeeds; then the idle channel being kept up, having been used to convey t...
kj::AsyncIoContext *const m_kj_io
KJ event loop context.
const session::Client_app & m_cli_app_ref
See ctor. Stored in case we must re-initialize m_session, if sync_connect() only partially succeeds.
~Client_context()
Destructor; in particular (assuming sync_connect() succeeded) shuts down the Vat_network and the Sess...
Client_context(flow::log::Logger *logger_ptr, kj::AsyncIoContext *kj_io, const session::Client_app &cli_app_ref, const session::Server_app &srv_app_ref, bool enable_hndl_transport=true)
Constructs us without establishing the capnp-RPC session.
bool sync_connect_impl(typename Session_obj::Channels *init_channels_by_cli_req_pre_sized, typename Session_obj::Channels *init_channels_by_srv_req, bool sans_shm_transport)
Impl of sync_connect() and sync_connect_sans_shm_transport().
typename Protocol::socket Peer_socket
Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
Provides Flow-IPC's integration with Cap'n Proto's Remote Procedure Call (RPC) layer,...
std::ostream & operator<<(std::ostream &os, const Client_context< Client_session_t > &val)
Prints string representation of the given Client_context to the given ostream.
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:131
Log_component
The flow::log::Component payload enumeration containing various log components used by Flow-IPC inter...
Definition: common.hpp:338
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
std::string m_name
Brief application name, readable to humans and unique across all other applications' names; used both...
Definition: app.hpp:179
An App that is used as a client in at least one client-server IPC split.
Definition: app.hpp:220
An App that is used as a server in at least one client-server IPC split.
Definition: app.hpp:241
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.
constexpr bool null() const noexcept
Returns true if and only if m_native_handle equals S_NULL_HANDLE.
void close() noexcept
Little utility that returns m_native_handle to the OS.