From 29d7ad94f1ae9f0b9cde55b4007aae0569d02e92 Mon Sep 17 00:00:00 2001 From: Kurt Sassenrath Date: Sun, 14 Jan 2024 23:17:43 -0800 Subject: [PATCH] Initial native client code. --- include/parselink/logging/formatters.h | 16 ++- include/parselink/logging/theme.h | 8 +- include/parselink/logging/traits.h | 26 +++-- include/parselink/msgpack/token/reader.h | 4 + include/parselink/msgpack/token/type.h | 4 + include/parselink/msgpack/token/views.h | 4 +- include/parselink/msgpack/token/writer.h | 4 + include/parselink/utility/argparse.h | 20 ++++ include/parselink/utility/ctstring.h | 118 ++++++++++++++++++++ source/BUILD | 31 ++++++ source/client/BUILD | 33 ++++++ source/client/client.cpp | 134 +++++++++++++++++++++++ source/client/client.h | 48 ++++++++ source/client/main.cpp | 71 ++++++++++++ source/logging/logging.cpp | 2 +- source/server/BUILD | 2 +- source/server/monolithic_server.cpp | 89 --------------- 17 files changed, 504 insertions(+), 110 deletions(-) create mode 100644 include/parselink/utility/ctstring.h create mode 100644 source/client/BUILD create mode 100644 source/client/client.cpp create mode 100644 source/client/client.h create mode 100644 source/client/main.cpp diff --git a/include/parselink/logging/formatters.h b/include/parselink/logging/formatters.h index 0dbcb03..3698878 100644 --- a/include/parselink/logging/formatters.h +++ b/include/parselink/logging/formatters.h @@ -27,12 +27,11 @@ #include // Simple wrapper for error strings to be formatted like any other string_view. -template <> -struct fmt::formatter - : fmt::formatter { +template +struct fmt::formatter : fmt::formatter { template constexpr auto format(auto const& v, FormatContext& ctx) const { - return fmt::formatter::format(v.v, ctx); + return fmt::formatter::format(v.message(), ctx); } }; @@ -150,4 +149,13 @@ struct fmt::formatter> } }; +template +struct fmt::formatter : fmt::formatter { + template + constexpr auto format(auto const& v, FormatContext& ctx) const { + return fmt::format_to( + ctx.out(), "{}:{}", v.address().to_string(), v.port()); + } +}; + #endif // logging_formatters_d22a64b1645a8134 diff --git a/include/parselink/logging/theme.h b/include/parselink/logging/theme.h index 1d22a52..3eb0518 100644 --- a/include/parselink/logging/theme.h +++ b/include/parselink/logging/theme.h @@ -98,14 +98,14 @@ template struct theme : static_theme {}; // Errors -template <> -struct theme : static_theme {}; +template +struct theme : static_theme {}; template <> struct theme : static_theme {}; -template <> -struct theme : static_theme {}; +template +struct theme : static_theme {}; template <> struct theme { diff --git a/include/parselink/logging/traits.h b/include/parselink/logging/traits.h index 71c14b7..753864e 100644 --- a/include/parselink/logging/traits.h +++ b/include/parselink/logging/traits.h @@ -22,6 +22,22 @@ #include #include +namespace { + +template +concept endpoint = requires(T const& t) { + { t.address() }; + { t.port() }; +}; + +template +concept error_code = requires(T const& t) { + { t.message() } -> std::convertible_to; + { t.category() }; +}; + +} // anonymous namespace + namespace parselink { namespace logging { @@ -41,16 +57,6 @@ struct enum_name_only { template enum_name_only(E) -> enum_name_only; -// Wrapper for a string that will be colorized as if it's an error, instead of -// a normal string. -struct error_str { - template T> - error_str(T&& t) - : v(std::forward(t)) {} - - std::string_view v; -}; - namespace detail { // The following concepts aim to describe both raw and smart pointers in a diff --git a/include/parselink/msgpack/token/reader.h b/include/parselink/msgpack/token/reader.h index 2d928df..4dc99f5 100644 --- a/include/parselink/msgpack/token/reader.h +++ b/include/parselink/msgpack/token/reader.h @@ -28,6 +28,10 @@ #include +#ifndef PARSELINK_MSGPACK_TOKEN_API +#define PARSELINK_MSGPACK_TOKEN_API +#endif + namespace msgpack { namespace detail { diff --git a/include/parselink/msgpack/token/type.h b/include/parselink/msgpack/token/type.h index abafb49..f730650 100644 --- a/include/parselink/msgpack/token/type.h +++ b/include/parselink/msgpack/token/type.h @@ -36,6 +36,10 @@ #include +#ifndef PARSELINK_MSGPACK_TOKEN_API +#define PARSELINK_MSGPACK_TOKEN_API +#endif + namespace msgpack { // This API is _currently_ optimizing on the fact that most desktop/server diff --git a/include/parselink/msgpack/token/views.h b/include/parselink/msgpack/token/views.h index 4b550b7..051f5ea 100644 --- a/include/parselink/msgpack/token/views.h +++ b/include/parselink/msgpack/token/views.h @@ -27,7 +27,9 @@ #include #include -#include +#ifndef PARSELINK_MSGPACK_TOKEN_API +#define PARSELINK_MSGPACK_TOKEN_API +#endif namespace msgpack { diff --git a/include/parselink/msgpack/token/writer.h b/include/parselink/msgpack/token/writer.h index 4440b10..1abe0cb 100644 --- a/include/parselink/msgpack/token/writer.h +++ b/include/parselink/msgpack/token/writer.h @@ -28,6 +28,10 @@ #include #include +#ifndef PARSELINK_MSGPACK_TOKEN_API +#define PARSELINK_MSGPACK_TOKEN_API +#endif + namespace msgpack { enum class writer_error { diff --git a/include/parselink/utility/argparse.h b/include/parselink/utility/argparse.h index 8dd26a8..1661e79 100644 --- a/include/parselink/utility/argparse.h +++ b/include/parselink/utility/argparse.h @@ -33,6 +33,8 @@ #include #include +#include + // Simple command line parser for testing executables. namespace argparse { @@ -260,6 +262,15 @@ public: : nullptr; } + template + T const& opt_or(std::string_view opt_name, T fallback = {}) const { + if (auto const* v = maybe_opt(opt_name)) { + return *v; + } else { + return fallback; + } + } + template T const& opt(std::string_view opt_name) const { auto const* v = maybe_opt(opt_name); @@ -287,6 +298,15 @@ public: return true; } + tl::expected try_parse( + std::span arglist) { + auto res = parse_inner(arglist); + if (res.ec != result::code::no_error) { + return tl::make_unexpected(res.ec); + } + return res; + } + result parse(std::span arglist) { return parse_inner(arglist); } diff --git a/include/parselink/utility/ctstring.h b/include/parselink/utility/ctstring.h new file mode 100644 index 0000000..0f14474 --- /dev/null +++ b/include/parselink/utility/ctstring.h @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Utility +// +// Compile-time string class, which can be used as template parameters and +// enable compile-time hashing/lookup of literals and so forth. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#include +#include + +namespace parselink { +namespace ct { + +template +struct string { + using char_type = char; // Maybe allow templatization in the future? + using storage_type = std::array; + storage_type data_ = {}; + + static constexpr auto npos = std::string_view::npos; + + constexpr string() noexcept = default; + + constexpr string(storage_type const& str) noexcept { + std::copy(std::begin(str), std::end(str), std::begin(data_)); + } + + constexpr string(char const (&str)[N + 1]) noexcept { + std::copy(std::begin(str), std::end(str), std::begin(data_)); + } + + constexpr string(string const& other) noexcept { + std::copy(std::begin(other), std::end(other), std::begin(data_)); + } + + constexpr string& operator=(string const& other) noexcept { + std::copy(std::begin(other), std::end(other), std::begin(data_)); + return *this; + } + + constexpr operator std::string_view() const noexcept { + return std::string_view{data_.data(), N}; + } + + // We save ourselves some headache by forwarding std::array's methods here. + constexpr auto data() noexcept { return data_.data(); } + + constexpr auto data() const noexcept { return data_.data(); } + + constexpr auto size() const noexcept { return N; } + + constexpr auto begin() noexcept { return data_.begin(); } + + constexpr auto begin() const noexcept { return data_.begin(); } + + constexpr auto end() noexcept { return data_.end() - 1; } + + constexpr auto end() const noexcept { return data_.end() - 1; } + + constexpr auto cbegin() const noexcept { return data_.cbegin(); } + + constexpr auto cend() const noexcept { return data_.cend() - 1; } + + constexpr auto find(char_type c) noexcept { + return std::string_view{*this}.find(c); + } + + constexpr auto find(char_type c) const noexcept { + return std::string_view{*this}.find(c); + } +}; + +template +string(char const (&)[N]) -> string; + +template +constexpr bool operator==(string const& a, string const& b) noexcept { + if constexpr (N1 != N2) return false; + return std::string_view{a} == std::string_view{b}; +} + +template +constexpr bool operator==(string const& a, std::string_view b) noexcept { + return std::string_view{a} == b; +} + +template +constexpr bool operator==(std::string_view a, string const& b) noexcept { + return b == a; +} + +template +consteval string operator+( + string const& a, string const& b) noexcept { + using char_type = typename std::decay_t::char_type; + char_type buff[N1 + N2 + 1]{}; + std::copy(std::begin(a), std::end(a), std::begin(buff)); + std::copy( + std::begin(b), std::end(b), std::next(std::begin(buff), a.size())); + return string(buff); +} + +// Sanity test for construction and concatenation at compile time. +static_assert(string("hello") + string(" world") == string("hello world")); + +} // namespace ct +} // namespace parselink diff --git a/source/BUILD b/source/BUILD index e69de29..3a01981 100644 --- a/source/BUILD +++ b/source/BUILD @@ -0,0 +1,31 @@ + +cc_library( + name = "client", + srcs = [ + "client.h", + "client.cpp", + ], + deps = [ + "//include/parselink:msgpack", + "//include/parselink:proto", + "//include/parselink:utility", + "//source/logging", + "//source/proto", + "@hydrogen", + "@boost//:asio", + "@boost//:beast", + ], + visibility = [ + "//visibility:private" + ], +) + +cc_binary( + name = "parselink", + srcs = [ + "main.cpp", + ], + deps = [ + "client" + ], +) diff --git a/source/client/BUILD b/source/client/BUILD new file mode 100644 index 0000000..50fdf70 --- /dev/null +++ b/source/client/BUILD @@ -0,0 +1,33 @@ +# parselink + +cc_library( + name = "client", + srcs = [ + "client.h", + "client.cpp", + ], + deps = [ + "//include/parselink:msgpack", + "//include/parselink:utility", + "//source/logging", + "//source/proto", + "@hydrogen", + "@boost//:asio", + "@boost//:beast", + ], + visibility = [ + # TODO: Fix visibility + "//visibility:public", + ], +) + +cc_binary( + name = "parselink", + srcs = [ + "client.h", + "main.cpp", + ], + deps = [ + "client" + ], +) diff --git a/source/client/client.cpp b/source/client/client.cpp new file mode 100644 index 0000000..26e4a14 --- /dev/null +++ b/source/client/client.cpp @@ -0,0 +1,134 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Client +// +// Client implementation. Most of this should be agnostic to the targeted +// platform (e.g. Linux vs Windows). +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + +#include "client.h" +#include "parselink/logging.h" +#include "parselink/utility/ctstring.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace parselink; +using namespace parselink::client; + +namespace net = boost::asio; +using net::awaitable; +using net::co_spawn; +using net::detached; +using net::use_awaitable; + +namespace { + +logging::logger logger{"client"}; + +constexpr auto no_ex_coro = net::as_tuple(use_awaitable); + +class simple_client { +public: + simple_client(config const& cfg) noexcept; + std::error_code run() noexcept; + +private: + awaitable connect_to_server() noexcept; + awaitable connect_to_websocket() noexcept; + + net::io_context io_context_; + std::string server_address_; + std::uint16_t server_port_; + std::string websocket_address_; + std::uint16_t websocket_port_; +}; + +static_assert(interface); + +simple_client::simple_client(config const& cfg) noexcept + : io_context_{1} { + server_address_ = + cfg.server_address.empty() ? "localhost" : cfg.server_address; + server_port_ = !cfg.server_port ? 9001 : cfg.server_port; + websocket_address_ = + cfg.websocket_address.empty() ? "localhost" : cfg.websocket_address; + websocket_port_ = !cfg.websocket_port ? 10501 : cfg.websocket_port; + + logger.debug("Creating parselink client. Configured server: {}:{}, " + "websocket: {}:{}.", + server_address_, server_port_, websocket_address_, websocket_port_); +}; + +awaitable simple_client::connect_to_server() noexcept { + logger.debug("Connecting to parselink server..."); + net::ip::tcp::resolver resolver(io_context_); + auto [ec, results] = co_await resolver.async_resolve( + {server_address_, std::to_string(server_port_)}, no_ex_coro); + + if (ec) { + logger.error("Unable to resolve {}:{}: {}", server_address_, + server_port_, ec); + co_return; + } else if (results.empty()) { + logger.error("Unable to resolve {}:{} to an endpoint.", server_address_, + server_port_); + co_return; + } + + net::ip::tcp::socket socket(io_context_); + + logger.debug("Attempting connection with {}", results.begin()->endpoint()); + auto [ec2] = co_await socket.async_connect(*results.begin(), no_ex_coro); + if (ec2) { + logger.error("connection to {} failed: {}", results.begin()->endpoint(), + ec2); + co_return; + } + + co_return; +} + +awaitable simple_client::connect_to_websocket() noexcept { + logger.debug("Connecting to websocket server..."); + co_return; +} + +std::error_code simple_client::run() noexcept { + logger.debug("Starting client."); + + net::signal_set signals(io_context_, SIGINT, SIGTERM); + signals.async_wait([&](auto sig, auto) { + logger.info("Received signal: {}. Shutting down.", sig); + }); + + co_spawn(io_context_, connect_to_websocket(), detached); + co_spawn(io_context_, connect_to_server(), detached); + + io_context_.run(); + + return std::make_error_code(std::errc::no_link); +}; + +} // anonymous namespace + +std::error_code parselink::client::create_and_run(config const& cfg) noexcept { + simple_client client(cfg); + return client.run(); +} diff --git a/source/client/client.h b/source/client/client.h new file mode 100644 index 0000000..d807922 --- /dev/null +++ b/source/client/client.h @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Client +// +// Client interface. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#ifndef client_d7b353c402e38bfe +#define client_d7b353c402e38bfe + +#include +#include +#include +#include + +namespace parselink { + +namespace client { + +struct config { + std::string server_address; + std::uint16_t server_port; + std::string websocket_address; + std::uint16_t websocket_port; +}; + +template +concept interface = + std::is_constructible_v && requires(T& client) { + { client.run() } -> std::same_as; + }; + +std::error_code create_and_run(config const& cfg) noexcept; + +} // namespace client +} // namespace parselink + +#endif // client_d7b353c402e38bfe diff --git a/source/client/main.cpp b/source/client/main.cpp new file mode 100644 index 0000000..e769323 --- /dev/null +++ b/source/client/main.cpp @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Client +// +// Client entrypoint. Should load config. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#include "client.h" + +#include "parselink/logging.h" +#include "parselink/utility/argparse.h" + +namespace { +parselink::logging::logger logger("main"); + +using level = parselink::logging::level; + +int run(std::span arg_list) { + argparse::command_line_parser parser({ + {"server", {std::string{"localhost"}}}, + {"server_port", {std::uint16_t{9001}}}, + {"websocket_port", {std::uint16_t{10501}}}, + {"websocket_server", {std::string{"localhost"}}}, + {"verbose", {false}}, + }); + + auto v = parser.try_parse(arg_list) + .map([](decltype(parser)::result args) { + if (args.opt("verbose")) { + logger.set_threshold(level::trace); + } + + parselink::client::config cfg; + cfg.server_address = args.opt("server"); + cfg.server_port = + args.opt("server_port"); + cfg.websocket_port = + args.opt("websocket_port"); + cfg.websocket_address = + args.opt("websocket_server"); + return parselink::client::create_and_run(cfg); + }) + .map([](auto err) { return err.value(); }) + .map_error([](auto ec) { + return int(std::errc::invalid_argument); + }); + return v ? *v : v.error(); +} + +} // namespace + +int main(int argc, char* argv[]) { + // TODO(ksassenrath): Add configuration file to the mix. + + std::vector args; + for (int i = 1; i < argc; ++i) { + args.emplace_back(argv[i]); + } + + return run(args); +} diff --git a/source/logging/logging.cpp b/source/logging/logging.cpp index 85f289a..b8079cf 100644 --- a/source/logging/logging.cpp +++ b/source/logging/logging.cpp @@ -26,7 +26,7 @@ namespace { struct console_endpoint : public endpoint { static constexpr std::string_view format_string = - "{:%Y-%m-%d %H:%M:%S}.{:03} [{:<8}] {:>20} | {}\n"; + "{:%Y-%m-%d %H:%M:%S}.{:03} [{:^8}] {:>20} | {}\n"; bool colored() const noexcept override { return true; } diff --git a/source/server/BUILD b/source/server/BUILD index 27a1308..87b94d9 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -6,8 +6,8 @@ cc_library( "memory_session_manager.cpp", ], deps = [ - "//include/parselink:proto", "//include/parselink:msgpack", + "//include/parselink:proto", "//include/parselink:server", "//source/logging", "@hydrogen", diff --git a/source/server/monolithic_server.cpp b/source/server/monolithic_server.cpp index f8ea983..965410e 100644 --- a/source/server/monolithic_server.cpp +++ b/source/server/monolithic_server.cpp @@ -57,24 +57,6 @@ using net::deferred; using net::detached; using net::use_awaitable; -//----------------------------------------------------------------------------- -// TODO(ksassenrath): These are logging formatters for various boost/asio types. -// Not all code is exposed to them, so they cannot be defined inside the -// generic logging/formatters.h header. They should go somewhere else. -//----------------------------------------------------------------------------- -template <> -struct parselink::logging::theme - : parselink::logging::static_theme {}; - -template <> -struct fmt::formatter - : fmt::formatter { - template - constexpr auto format(auto const& v, FormatContext& ctx) const { - return fmt::formatter::format(v.message(), ctx); - } -}; - template <> struct fmt::formatter : fmt::formatter { constexpr auto format(auto const&, auto& ctx) const { @@ -82,77 +64,6 @@ struct fmt::formatter : fmt::formatter { } }; -template <> -struct fmt::formatter { - template - constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { - return ctx.begin(); - } - - template - auto format(msgpack::token const& v, FormatContext& ctx) const { - using parselink::logging::themed_arg; - auto out = fmt::format_to( - ctx.out(), "()))); - break; - case msgpack::format::type::signed_int: - out = fmt::format_to( - out, "{}", themed_arg(*(v.get()))); - break; - case msgpack::format::type::boolean: - out = fmt::format_to(out, "{}", themed_arg(*(v.get()))); - break; - case msgpack::format::type::string: - out = fmt::format_to( - out, "{}", themed_arg(*(v.get()))); - break; - case msgpack::format::type::binary: - out = fmt::format_to(out, "{}", - themed_arg(*(v.get>()))); - break; - case msgpack::format::type::map: - out = fmt::format_to(out, "(arity: {})", - themed_arg(v.get()->count)); - break; - case msgpack::format::type::array: - out = fmt::format_to(out, "(arity: {})", - themed_arg(v.get()->count)); - break; - case msgpack::format::type::nil: - out = fmt::format_to(out, "(nil)"); - break; - case msgpack::format::type::invalid: - out = fmt::format_to(out, "(invalid)"); - break; - default: break; - } - return fmt::format_to(out, ">"); - } -}; - -template -concept endpoint = requires(T const& t) { - { t.address() }; - { t.port() }; -}; - -template -struct parselink::logging::theme - : parselink::logging::static_theme {}; - -template -struct fmt::formatter : fmt::formatter { - template - constexpr auto format(auto const& v, FormatContext& ctx) const { - return fmt::format_to( - ctx.out(), "{}:{}", v.address().to_string(), v.port()); - } -}; - //----------------------------------------------------------------------------- // End formatters //-----------------------------------------------------------------------------