diff options
Diffstat (limited to 'src/ipc')
-rw-r--r-- | src/ipc/CMakeLists.txt | 7 | ||||
-rw-r--r-- | src/ipc/capnp/common-types.h | 139 | ||||
-rw-r--r-- | src/ipc/capnp/common.capnp | 16 | ||||
-rw-r--r-- | src/ipc/capnp/init-types.h | 1 | ||||
-rw-r--r-- | src/ipc/capnp/init.capnp | 3 | ||||
-rw-r--r-- | src/ipc/capnp/mining-types.h | 26 | ||||
-rw-r--r-- | src/ipc/capnp/mining.capnp | 52 | ||||
-rw-r--r-- | src/ipc/capnp/mining.cpp | 47 | ||||
-rw-r--r-- | src/ipc/capnp/protocol.cpp | 13 | ||||
-rw-r--r-- | src/ipc/interfaces.cpp | 30 | ||||
-rw-r--r-- | src/ipc/process.cpp | 96 | ||||
-rw-r--r-- | src/ipc/process.h | 10 | ||||
-rw-r--r-- | src/ipc/protocol.h | 28 |
13 files changed, 423 insertions, 45 deletions
diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt index 94b1ceb54e..904d72f56e 100644 --- a/src/ipc/CMakeLists.txt +++ b/src/ipc/CMakeLists.txt @@ -3,16 +3,21 @@ # file COPYING or https://opensource.org/license/mit/. add_library(bitcoin_ipc STATIC EXCLUDE_FROM_ALL + capnp/mining.cpp capnp/protocol.cpp interfaces.cpp process.cpp ) target_capnp_sources(bitcoin_ipc ${PROJECT_SOURCE_DIR} - capnp/echo.capnp capnp/init.capnp + capnp/common.capnp + capnp/echo.capnp + capnp/init.capnp + capnp/mining.capnp ) target_link_libraries(bitcoin_ipc PRIVATE core_interface + univalue ) diff --git a/src/ipc/capnp/common-types.h b/src/ipc/capnp/common-types.h index 39e368491b..51af6a5f0a 100644 --- a/src/ipc/capnp/common-types.h +++ b/src/ipc/capnp/common-types.h @@ -6,6 +6,9 @@ #define BITCOIN_IPC_CAPNP_COMMON_TYPES_H #include <clientversion.h> +#include <interfaces/types.h> +#include <primitives/transaction.h> +#include <serialize.h> #include <streams.h> #include <univalue.h> @@ -16,33 +19,24 @@ namespace ipc { namespace capnp { -//! Use SFINAE to define Serializeable<T> trait which is true if type T has a -//! Serialize(stream) method, false otherwise. -template <typename T> -struct Serializable { -private: - template <typename C> - static std::true_type test(decltype(std::declval<C>().Serialize(std::declval<std::nullptr_t&>()))*); - template <typename> - static std::false_type test(...); - -public: - static constexpr bool value = decltype(test<T>(nullptr))::value; -}; +//! Construct a ParamStream wrapping a data stream with serialization parameters +//! needed to pass transaction objects between bitcoin processes. +//! In the future, more params may be added here to serialize other objects that +//! require serialization parameters. Params should just be chosen to serialize +//! objects completely and ensure that serializing and deserializing objects +//! with the specified parameters produces equivalent objects. It's also +//! harmless to specify serialization parameters here that are not used. +template <typename S> +auto Wrap(S& s) +{ + return ParamsStream{s, TX_WITH_WITNESS}; +} -//! Use SFINAE to define Unserializeable<T> trait which is true if type T has -//! an Unserialize(stream) method, false otherwise. +//! Detect if type has a deserialize_type constructor, which is +//! used to deserialize types like CTransaction that can't be unserialized into +//! existing objects because they are immutable. template <typename T> -struct Unserializable { -private: - template <typename C> - static std::true_type test(decltype(std::declval<C>().Unserialize(std::declval<std::nullptr_t&>()))*); - template <typename> - static std::false_type test(...); - -public: - static constexpr bool value = decltype(test<T>(nullptr))::value; -}; +concept Deserializable = std::is_constructible_v<T, ::deserialize_type, ::DataStream&>; } // namespace capnp } // namespace ipc @@ -50,42 +44,78 @@ public: namespace mp { //! Overload multiprocess library's CustomBuildField hook to allow any //! serializable object to be stored in a capnproto Data field or passed to a -//! canproto interface. Use Priority<1> so this hook has medium priority, and +//! capnproto interface. Use Priority<1> so this hook has medium priority, and //! higher priority hooks could take precedence over this one. template <typename LocalType, typename Value, typename Output> -void CustomBuildField( - TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output, - // Enable if serializeable and if LocalType is not cv or reference - // qualified. If LocalType is cv or reference qualified, it is important to - // fall back to lower-priority Priority<0> implementation of this function - // that strips cv references, to prevent this CustomBuildField overload from - // taking precedence over more narrow overloads for specific LocalTypes. - std::enable_if_t<ipc::capnp::Serializable<LocalType>::value && - std::is_same_v<LocalType, std::remove_cv_t<std::remove_reference_t<LocalType>>>>* enable = nullptr) +void CustomBuildField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output) +// Enable if serializeable and if LocalType is not cv or reference qualified. If +// LocalType is cv or reference qualified, it is important to fall back to +// lower-priority Priority<0> implementation of this function that strips cv +// references, to prevent this CustomBuildField overload from taking precedence +// over more narrow overloads for specific LocalTypes. +requires Serializable<LocalType, DataStream> && std::is_same_v<LocalType, std::remove_cv_t<std::remove_reference_t<LocalType>>> { DataStream stream; - value.Serialize(stream); + auto wrapper{ipc::capnp::Wrap(stream)}; + value.Serialize(wrapper); auto result = output.init(stream.size()); memcpy(result.begin(), stream.data(), stream.size()); } //! Overload multiprocess library's CustomReadField hook to allow any object //! with an Unserialize method to be read from a capnproto Data field or -//! returned from canproto interface. Use Priority<1> so this hook has medium +//! returned from capnproto interface. Use Priority<1> so this hook has medium //! priority, and higher priority hooks could take precedence over this one. template <typename LocalType, typename Input, typename ReadDest> -decltype(auto) -CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest, - std::enable_if_t<ipc::capnp::Unserializable<LocalType>::value>* enable = nullptr) +decltype(auto) CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest) +requires Unserializable<LocalType, DataStream> && (!ipc::capnp::Deserializable<LocalType>) { return read_dest.update([&](auto& value) { if (!input.has()) return; auto data = input.get(); SpanReader stream({data.begin(), data.end()}); - value.Unserialize(stream); + auto wrapper{ipc::capnp::Wrap(stream)}; + value.Unserialize(wrapper); }); } +//! Overload multiprocess library's CustomReadField hook to allow any object +//! with a deserialize constructor to be read from a capnproto Data field or +//! returned from capnproto interface. Use Priority<1> so this hook has medium +//! priority, and higher priority hooks could take precedence over this one. +template <typename LocalType, typename Input, typename ReadDest> +decltype(auto) CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest) +requires ipc::capnp::Deserializable<LocalType> +{ + assert(input.has()); + auto data = input.get(); + SpanReader stream({data.begin(), data.end()}); + auto wrapper{ipc::capnp::Wrap(stream)}; + return read_dest.construct(::deserialize, wrapper); +} + +//! Overload CustomBuildField and CustomReadField to serialize std::chrono +//! parameters and return values as numbers. +template <class Rep, class Period, typename Value, typename Output> +void CustomBuildField(TypeList<std::chrono::duration<Rep, Period>>, Priority<1>, InvokeContext& invoke_context, Value&& value, + Output&& output) +{ + static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(), + "capnp type does not have enough range to hold lowest std::chrono::duration value"); + static_assert(std::numeric_limits<decltype(output.get())>::max() >= std::numeric_limits<Rep>::max(), + "capnp type does not have enough range to hold highest std::chrono::duration value"); + output.set(value.count()); +} + +template <class Rep, class Period, typename Input, typename ReadDest> +decltype(auto) CustomReadField(TypeList<std::chrono::duration<Rep, Period>>, Priority<1>, InvokeContext& invoke_context, + Input&& input, ReadDest&& read_dest) +{ + return read_dest.construct(input.get()); +} + +//! Overload CustomBuildField and CustomReadField to serialize UniValue +//! parameters and return values as JSON strings. template <typename Value, typename Output> void CustomBuildField(TypeList<UniValue>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output) { @@ -103,6 +133,33 @@ decltype(auto) CustomReadField(TypeList<UniValue>, Priority<1>, InvokeContext& i value.read(std::string_view{data.begin(), data.size()}); }); } + +//! Generic ::capnp::Data field builder for any C++ type that can be converted +//! to a span of bytes, like std::vector<char> or std::array<uint8_t>, or custom +//! blob types like uint256 or PKHash with data() and size() methods pointing to +//! bytes. +//! +//! Note: it might make sense to move this function into libmultiprocess, since +//! it is fairly generic. However this would require decreasing its priority so +//! it can be overridden, which would require more changes inside +//! libmultiprocess to avoid conflicting with the Priority<1> CustomBuildField +//! function it already provides for std::vector. Also, it might make sense to +//! provide a CustomReadField counterpart to this function, which could be +//! called to read C++ types that can be constructed from spans of bytes from +//! ::capnp::Data fields. But so far there hasn't been a need for this. +template <typename LocalType, typename Value, typename Output> +void CustomBuildField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Value&& value, Output&& output) +requires + (std::is_same_v<decltype(output.get()), ::capnp::Data::Builder>) && + (std::convertible_to<Value, std::span<const std::byte>> || + std::convertible_to<Value, std::span<const char>> || + std::convertible_to<Value, std::span<const unsigned char>> || + std::convertible_to<Value, std::span<const signed char>>) +{ + auto data = std::span{value}; + auto result = output.init(data.size()); + memcpy(result.begin(), data.data(), data.size()); +} } // namespace mp #endif // BITCOIN_IPC_CAPNP_COMMON_TYPES_H diff --git a/src/ipc/capnp/common.capnp b/src/ipc/capnp/common.capnp new file mode 100644 index 0000000000..b3359f3f07 --- /dev/null +++ b/src/ipc/capnp/common.capnp @@ -0,0 +1,16 @@ +# Copyright (c) 2024 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +@0xcd2c6232cb484a28; + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("ipc::capnp::messages"); + +using Proxy = import "/mp/proxy.capnp"; +$Proxy.includeTypes("ipc/capnp/common-types.h"); + +struct BlockRef $Proxy.wrap("interfaces::BlockRef") { + hash @0 :Data; + height @1 :Int32; +} diff --git a/src/ipc/capnp/init-types.h b/src/ipc/capnp/init-types.h index 42031441b5..c3ddca27c0 100644 --- a/src/ipc/capnp/init-types.h +++ b/src/ipc/capnp/init-types.h @@ -6,5 +6,6 @@ #define BITCOIN_IPC_CAPNP_INIT_TYPES_H #include <ipc/capnp/echo.capnp.proxy-types.h> +#include <ipc/capnp/mining.capnp.proxy-types.h> #endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H diff --git a/src/ipc/capnp/init.capnp b/src/ipc/capnp/init.capnp index e6d358c665..1001ee5336 100644 --- a/src/ipc/capnp/init.capnp +++ b/src/ipc/capnp/init.capnp @@ -10,11 +10,14 @@ $Cxx.namespace("ipc::capnp::messages"); using Proxy = import "/mp/proxy.capnp"; $Proxy.include("interfaces/echo.h"); $Proxy.include("interfaces/init.h"); +$Proxy.include("interfaces/mining.h"); $Proxy.includeTypes("ipc/capnp/init-types.h"); using Echo = import "echo.capnp"; +using Mining = import "mining.capnp"; interface Init $Proxy.wrap("interfaces::Init") { construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap); makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo); + makeMining @2 (context :Proxy.Context) -> (result :Mining.Mining); } diff --git a/src/ipc/capnp/mining-types.h b/src/ipc/capnp/mining-types.h new file mode 100644 index 0000000000..2e60b43fcf --- /dev/null +++ b/src/ipc/capnp/mining-types.h @@ -0,0 +1,26 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_IPC_CAPNP_MINING_TYPES_H +#define BITCOIN_IPC_CAPNP_MINING_TYPES_H + +#include <interfaces/mining.h> +#include <ipc/capnp/common.capnp.proxy-types.h> +#include <ipc/capnp/common-types.h> +#include <ipc/capnp/mining.capnp.proxy.h> +#include <node/miner.h> +#include <node/types.h> +#include <validation.h> + +namespace mp { +// Custom serialization for BlockValidationState. +void CustomBuildMessage(InvokeContext& invoke_context, + const BlockValidationState& src, + ipc::capnp::messages::BlockValidationState::Builder&& builder); +void CustomReadMessage(InvokeContext& invoke_context, + const ipc::capnp::messages::BlockValidationState::Reader& reader, + BlockValidationState& dest); +} // namespace mp + +#endif // BITCOIN_IPC_CAPNP_MINING_TYPES_H diff --git a/src/ipc/capnp/mining.capnp b/src/ipc/capnp/mining.capnp new file mode 100644 index 0000000000..5e0216acea --- /dev/null +++ b/src/ipc/capnp/mining.capnp @@ -0,0 +1,52 @@ +# Copyright (c) 2024 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +@0xc77d03df6a41b505; + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("ipc::capnp::messages"); + +using Common = import "common.capnp"; +using Proxy = import "/mp/proxy.capnp"; +$Proxy.include("interfaces/mining.h"); +$Proxy.includeTypes("ipc/capnp/mining-types.h"); + +interface Mining $Proxy.wrap("interfaces::Mining") { + isTestChain @0 (context :Proxy.Context) -> (result: Bool); + isInitialBlockDownload @1 (context :Proxy.Context) -> (result: Bool); + getTip @2 (context :Proxy.Context) -> (result: Common.BlockRef, hasResult: Bool); + waitTipChanged @3 (context :Proxy.Context, currentTip: Data, timeout: Float64) -> (result: Common.BlockRef); + createNewBlock @4 (scriptPubKey: Data, options: BlockCreateOptions) -> (result: BlockTemplate); + processNewBlock @5 (context :Proxy.Context, block: Data) -> (newBlock: Bool, result: Bool); + getTransactionsUpdated @6 (context :Proxy.Context) -> (result: UInt32); + testBlockValidity @7 (context :Proxy.Context, block: Data, checkMerkleRoot: Bool) -> (state: BlockValidationState, result: Bool); +} + +interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") { + getBlockHeader @0 (context: Proxy.Context) -> (result: Data); + getBlock @1 (context: Proxy.Context) -> (result: Data); + getTxFees @2 (context: Proxy.Context) -> (result: List(Int64)); + getTxSigops @3 (context: Proxy.Context) -> (result: List(Int64)); + getCoinbaseTx @4 (context: Proxy.Context) -> (result: Data); + getCoinbaseCommitment @5 (context: Proxy.Context) -> (result: Data); + getWitnessCommitmentIndex @6 (context: Proxy.Context) -> (result: Int32); + getCoinbaseMerklePath @7 (context: Proxy.Context) -> (result: List(Data)); + submitSolution@8 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (result: Bool); +} + +struct BlockCreateOptions $Proxy.wrap("node::BlockCreateOptions") { + useMempool @0 :Bool $Proxy.name("use_mempool"); + coinbaseMaxAdditionalWeight @1 :UInt64 $Proxy.name("coinbase_max_additional_weight"); + coinbaseOutputMaxAdditionalSigops @2 :UInt64 $Proxy.name("coinbase_output_max_additional_sigops"); +} + +# Note: serialization of the BlockValidationState C++ type is somewhat fragile +# and using the struct can be awkward. It would be good if testBlockValidity +# method were changed to return validity information in a simpler format. +struct BlockValidationState { + mode @0 :Int32; + result @1 :Int32; + rejectReason @2 :Text; + debugMessage @3 :Text; +} diff --git a/src/ipc/capnp/mining.cpp b/src/ipc/capnp/mining.cpp new file mode 100644 index 0000000000..0f9533c1c7 --- /dev/null +++ b/src/ipc/capnp/mining.cpp @@ -0,0 +1,47 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <ipc/capnp/mining-types.h> +#include <ipc/capnp/mining.capnp.proxy-types.h> + +#include <mp/proxy-types.h> + +namespace mp { +void CustomBuildMessage(InvokeContext& invoke_context, + const BlockValidationState& src, + ipc::capnp::messages::BlockValidationState::Builder&& builder) +{ + if (src.IsValid()) { + builder.setMode(0); + } else if (src.IsInvalid()) { + builder.setMode(1); + } else if (src.IsError()) { + builder.setMode(2); + } else { + assert(false); + } + builder.setResult(static_cast<int>(src.GetResult())); + builder.setRejectReason(src.GetRejectReason()); + builder.setDebugMessage(src.GetDebugMessage()); +} + +void CustomReadMessage(InvokeContext& invoke_context, + const ipc::capnp::messages::BlockValidationState::Reader& reader, + BlockValidationState& dest) +{ + if (reader.getMode() == 0) { + assert(reader.getResult() == 0); + assert(reader.getRejectReason().size() == 0); + assert(reader.getDebugMessage().size() == 0); + } else if (reader.getMode() == 1) { + dest.Invalid(static_cast<BlockValidationResult>(reader.getResult()), reader.getRejectReason(), reader.getDebugMessage()); + } else if (reader.getMode() == 2) { + assert(reader.getResult() == 0); + dest.Error(reader.getRejectReason()); + assert(reader.getDebugMessage().size() == 0); + } else { + assert(false); + } +} +} // namespace mp diff --git a/src/ipc/capnp/protocol.cpp b/src/ipc/capnp/protocol.cpp index 73276d6d90..4b67a5bd1e 100644 --- a/src/ipc/capnp/protocol.cpp +++ b/src/ipc/capnp/protocol.cpp @@ -23,6 +23,8 @@ #include <mutex> #include <optional> #include <string> +#include <sys/socket.h> +#include <system_error> #include <thread> namespace ipc { @@ -51,11 +53,20 @@ public: startLoop(exe_name); return mp::ConnectStream<messages::Init>(*m_loop, fd); } - void serve(int fd, const char* exe_name, interfaces::Init& init) override + void listen(int listen_fd, const char* exe_name, interfaces::Init& init) override + { + startLoop(exe_name); + if (::listen(listen_fd, /*backlog=*/5) != 0) { + throw std::system_error(errno, std::system_category()); + } + mp::ListenConnections<messages::Init>(*m_loop, listen_fd, init); + } + void serve(int fd, const char* exe_name, interfaces::Init& init, const std::function<void()>& ready_fn = {}) override { assert(!m_loop); mp::g_thread_context.thread_name = mp::ThreadName(exe_name); m_loop.emplace(exe_name, &IpcLogFn, &m_context); + if (ready_fn) ready_fn(); mp::ServeStream<messages::Init>(*m_loop, fd, init); m_loop->loop(); m_loop.reset(); diff --git a/src/ipc/interfaces.cpp b/src/ipc/interfaces.cpp index b409443f64..33555f05d4 100644 --- a/src/ipc/interfaces.cpp +++ b/src/ipc/interfaces.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include <common/args.h> #include <common/system.h> #include <interfaces/init.h> #include <interfaces/ipc.h> @@ -56,6 +57,35 @@ public: exit_status = EXIT_SUCCESS; return true; } + std::unique_ptr<interfaces::Init> connectAddress(std::string& address) override + { + if (address.empty() || address == "0") return nullptr; + int fd; + if (address == "auto") { + // Treat "auto" the same as "unix" except don't treat it an as error + // if the connection is not accepted. Just return null so the caller + // can work offline without a connection, or spawn a new + // bitcoin-node process and connect to it. + address = "unix"; + try { + fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address); + } catch (const std::system_error& e) { + // If connection type is auto and socket path isn't accepting connections, or doesn't exist, catch the error and return null; + if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory) { + return nullptr; + } + throw; + } + } else { + fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address); + } + return m_protocol->connect(fd, m_exe_name); + } + void listenAddress(std::string& address) override + { + int fd = m_process->bind(gArgs.GetDataDirNet(), m_exe_name, address); + m_protocol->listen(fd, m_exe_name, m_init); + } void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override { m_protocol->addCleanup(type, iface, std::move(cleanup)); diff --git a/src/ipc/process.cpp b/src/ipc/process.cpp index 9657dcd092..432c365d8f 100644 --- a/src/ipc/process.cpp +++ b/src/ipc/process.cpp @@ -4,22 +4,28 @@ #include <ipc/process.h> #include <ipc/protocol.h> +#include <logging.h> #include <mp/util.h> #include <tinyformat.h> #include <util/fs.h> #include <util/strencodings.h> +#include <util/syserror.h> #include <cstdint> #include <cstdlib> +#include <errno.h> #include <exception> #include <iostream> #include <stdexcept> #include <string.h> -#include <system_error> +#include <sys/socket.h> +#include <sys/un.h> #include <unistd.h> #include <utility> #include <vector> +using util::RemovePrefixView; + namespace ipc { namespace { class ProcessImpl : public Process @@ -54,7 +60,95 @@ public: } return true; } + int connect(const fs::path& data_dir, + const std::string& dest_exe_name, + std::string& address) override; + int bind(const fs::path& data_dir, const std::string& exe_name, std::string& address) override; }; + +static bool ParseAddress(std::string& address, + const fs::path& data_dir, + const std::string& dest_exe_name, + struct sockaddr_un& addr, + std::string& error) +{ + if (address.compare(0, 4, "unix") == 0 && (address.size() == 4 || address[4] == ':')) { + fs::path path; + if (address.size() <= 5) { + path = data_dir / fs::PathFromString(strprintf("%s.sock", RemovePrefixView(dest_exe_name, "bitcoin-"))); + } else { + path = data_dir / fs::PathFromString(address.substr(5)); + } + std::string path_str = fs::PathToString(path); + address = strprintf("unix:%s", path_str); + if (path_str.size() >= sizeof(addr.sun_path)) { + error = strprintf("Unix address path %s exceeded maximum socket path length", fs::quoted(fs::PathToString(path))); + return false; + } + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path_str.c_str(), sizeof(addr.sun_path)-1); + return true; + } + + error = strprintf("Unrecognized address '%s'", address); + return false; +} + +int ProcessImpl::connect(const fs::path& data_dir, + const std::string& dest_exe_name, + std::string& address) +{ + struct sockaddr_un addr; + std::string error; + if (!ParseAddress(address, data_dir, dest_exe_name, addr, error)) { + throw std::invalid_argument(error); + } + + int fd; + if ((fd = ::socket(addr.sun_family, SOCK_STREAM, 0)) == -1) { + throw std::system_error(errno, std::system_category()); + } + if (::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) { + return fd; + } + int connect_error = errno; + if (::close(fd) != 0) { + LogPrintf("Error closing file descriptor %i '%s': %s\n", fd, address, SysErrorString(errno)); + } + throw std::system_error(connect_error, std::system_category()); +} + +int ProcessImpl::bind(const fs::path& data_dir, const std::string& exe_name, std::string& address) +{ + struct sockaddr_un addr; + std::string error; + if (!ParseAddress(address, data_dir, exe_name, addr, error)) { + throw std::invalid_argument(error); + } + + if (addr.sun_family == AF_UNIX) { + fs::path path = addr.sun_path; + if (path.has_parent_path()) fs::create_directories(path.parent_path()); + if (fs::symlink_status(path).type() == fs::file_type::socket) { + fs::remove(path); + } + } + + int fd; + if ((fd = ::socket(addr.sun_family, SOCK_STREAM, 0)) == -1) { + throw std::system_error(errno, std::system_category()); + } + + if (::bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) { + return fd; + } + int bind_error = errno; + if (::close(fd) != 0) { + LogPrintf("Error closing file descriptor %i: %s\n", fd, SysErrorString(errno)); + } + throw std::system_error(bind_error, std::system_category()); +} } // namespace std::unique_ptr<Process> MakeProcess() { return std::make_unique<ProcessImpl>(); } diff --git a/src/ipc/process.h b/src/ipc/process.h index 40f2d2acf6..2ed8b73fab 100644 --- a/src/ipc/process.h +++ b/src/ipc/process.h @@ -34,6 +34,16 @@ public: //! process. If so, return true and a file descriptor for communicating //! with the parent process. virtual bool checkSpawned(int argc, char* argv[], int& fd) = 0; + + //! Canonicalize and connect to address, returning socket descriptor. + virtual int connect(const fs::path& data_dir, + const std::string& dest_exe_name, + std::string& address) = 0; + + //! Create listening socket, bind and canonicalize address, and return socket descriptor. + virtual int bind(const fs::path& data_dir, + const std::string& exe_name, + std::string& address) = 0; }; //! Constructor for Process interface. Implementation will vary depending on diff --git a/src/ipc/protocol.h b/src/ipc/protocol.h index 4cd892e411..b2ebf99e8c 100644 --- a/src/ipc/protocol.h +++ b/src/ipc/protocol.h @@ -25,12 +25,38 @@ public: //! Return Init interface that forwards requests over given socket descriptor. //! Socket communication is handled on a background thread. + //! + //! @note It could be potentially useful in the future to add + //! std::function<void()> on_disconnect callback argument here. But there + //! isn't an immediate need, because the protocol implementation can clean + //! up its own state (calling ProxyServer destructors, etc) on disconnect, + //! and any client calls will just throw ipc::Exception errors after a + //! disconnect. virtual std::unique_ptr<interfaces::Init> connect(int fd, const char* exe_name) = 0; + //! Listen for connections on provided socket descriptor, accept them, and + //! handle requests on accepted connections. This method doesn't block, and + //! performs I/O on a background thread. + virtual void listen(int listen_fd, const char* exe_name, interfaces::Init& init) = 0; + //! Handle requests on provided socket descriptor, forwarding them to the //! provided Init interface. Socket communication is handled on the //! current thread, and this call blocks until the socket is closed. - virtual void serve(int fd, const char* exe_name, interfaces::Init& init) = 0; + //! + //! @note: If this method is called, it needs be called before connect() or + //! listen() methods, because for ease of implementation it's inflexible and + //! always runs the event loop in the foreground thread. It can share its + //! event loop with the other methods but can't share an event loop that was + //! created by them. This isn't really a problem because serve() is only + //! called by spawned child processes that call it immediately to + //! communicate back with parent processes. + // + //! The optional `ready_fn` callback will be called after the event loop is + //! created but before it is started. This can be useful in tests to trigger + //! client connections from another thread as soon as the event loop is + //! available, but should not be neccessary in normal code which starts + //! clients and servers independently. + virtual void serve(int fd, const char* exe_name, interfaces::Init& init, const std::function<void()>& ready_fn = {}) = 0; //! Add cleanup callback to interface that will run when the interface is //! deleted. |