diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/CMakeLists.txt | 11 | ||||
-rw-r--r-- | server/error.hpp | 15 | ||||
-rw-r--r-- | server/lexer.hpp | 261 | ||||
-rw-r--r-- | server/log.hpp | 49 | ||||
-rw-r--r-- | server/main.cpp | 34 | ||||
-rw-r--r-- | server/parser.hpp | 168 | ||||
-rw-r--r-- | server/server.cpp | 111 | ||||
-rw-r--r-- | server/server.hpp | 34 | ||||
-rw-r--r-- | server/session.cpp | 113 | ||||
-rw-r--r-- | server/session.hpp | 42 | ||||
-rw-r--r-- | server/session_manager.cpp | 37 | ||||
-rw-r--r-- | server/session_manager.hpp | 30 | ||||
-rw-r--r-- | server/settings.hpp | 87 |
13 files changed, 992 insertions, 0 deletions
diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..7bf356a --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,11 @@ +find_package(Boost REQUIRED COMPONENTS filesystem program_options) + +option(DEBUG_ASIO "enable debug output for Boost.Asio" OFF) + +add_executable(server main.cpp server.cpp session.cpp session_manager.cpp) +target_include_directories(server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) +target_link_libraries(server PRIVATE ${Boost_LIBRARIES}) + +if(DEBUG_ASIO) + target_compile_definitions(server PRIVATE BOOST_ASIO_ENABLE_HANDLER_TRACKING) +endif() diff --git a/server/error.hpp b/server/error.hpp new file mode 100644 index 0000000..cbfbb1e --- /dev/null +++ b/server/error.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <stdexcept> +#include <string> + +namespace math::server { + +class Error : public std::runtime_error { +public: + explicit Error(const std::string& what) + : std::runtime_error{"server error: " + what} + { } +}; + +} diff --git a/server/lexer.hpp b/server/lexer.hpp new file mode 100644 index 0000000..8afe15c --- /dev/null +++ b/server/lexer.hpp @@ -0,0 +1,261 @@ +#pragma once + +#include "error.hpp" + +#include <cmath> + +#include <exception> +#include <functional> +#include <limits> +#include <optional> +#include <regex> +#include <string> +#include <string_view> +#include <unordered_map> +#include <vector> + +namespace math::server { +namespace lexer { + +class Error : public server::Error { +public: + explicit Error(const std::string &what) + : server::Error{"lexer error: " + what} + { } +}; + +class Token { +public: + enum class Type { + LEFT_PAREN, + RIGHT_PAREN, + PLUS, + MINUS, + ASTERISK, + SLASH, + NUMBER, + }; + + explicit Token(Type type) + : m_type{type}, m_number_value{nan()} + { } + + explicit Token(double number_value) + : m_type{Type::NUMBER}, m_number_value{number_value} + { } + + bool operator==(const Token& other) const { + return m_type == other.m_type + && ((is_nan(m_number_value) && is_nan(other.m_number_value)) + || m_number_value == other.m_number_value); + } + + bool operator!=(const Token& other) const { return !(*this == other); } + + Type get_type() const { return m_type; } + + double get_number_value() const { + if (get_type() != Type::NUMBER) { + throw Error{"token must be a number to query its value"}; + } + return m_number_value; + } + +private: + static constexpr double nan() { return std::numeric_limits<double>::quiet_NaN(); } + + static bool is_nan(double x) { return std::isnan(x); } + + Type m_type; + double m_number_value; +}; + +namespace details { + +inline std::string_view match_number(const std::string_view& input) { + static constexpr std::regex::flag_type flags = + std::regex_constants::ECMAScript | + std::regex_constants::icase; + // This is a hacky attempt to describe a C-like grammar for floating-point + // numbers using a regex (the tests seem to pass though). + // A proper NFA would be better, I guess. + static const std::regex number_regex{R"REGEX(^(?:\d+(?:\.\d*)?|\.\d+)(e[+-]?(\d*))?)REGEX", flags}; + + std::cmatch match; + if (!std::regex_search(input.cbegin(), input.cend(), match, number_regex)) { + return {}; + } + // If we have the numeric part of a number followed by 'e' and no digits, + // 1) that 'e' definitely belongs to this number token, + // 2) the user forgot to type in the required digits. + const auto& exponent = match[1]; + const auto& abs_power = match[2]; + if (exponent.matched && abs_power.matched && abs_power.length() == 0) { + throw lexer::Error{"exponent has no digits: " + match[0].str()}; + } + return {match[0].first, match[0].length()}; +} + +inline std::optional<double> parse_number(const std::string_view& input, std::string_view& token) { + const auto view = match_number(input); + if (!view.data()) { + return {}; + } + try { + const auto result = std::stod(std::string{view}); + token = view; + return result; + } catch (const std::exception& e) { + throw lexer::Error{"couldn't parse number from: " + std::string{view}}; + } + return {}; +} + +inline std::optional<double> parse_number(const std::string_view& input) { + std::string_view token; + return parse_number(input, token); +} + +inline bool starts_with(const std::string_view& a, const std::string_view& b) noexcept { + return a.length() >= b.length() + && a.compare(0, b.length(), b) == 0; +} + +inline std::optional<Token::Type> parse_const_token(const std::string_view& input, std::string_view& token) { + // FIXME: Potentially error-prone if there's const token A which is a + // prefix of token B (if the map is not sorted, we'd parse token A, when it + // could've been token B). + // Can be solved by sorting the keys accordingly. + + static const std::unordered_map<std::string_view, Token::Type> const_tokens{ + {"(", Token::Type::LEFT_PAREN}, + {")", Token::Type::RIGHT_PAREN}, + {"+", Token::Type::PLUS}, + {"-", Token::Type::MINUS}, + {"*", Token::Type::ASTERISK}, + {"/", Token::Type::SLASH}, + }; + + for (const auto& it : const_tokens) { + const auto& str = it.first; + const auto& type = it.second; + + if (starts_with(input, str)) { + token = input.substr(0, str.length()); + return type; + } + } + + return {}; +} + +inline std::optional<Token::Type> parse_const_token(const std::string_view& input) { + std::string_view token; + return parse_const_token(input, token); +} + +inline std::string_view parse_whitespace(const std::string_view& input) { + static const std::regex ws_regex{R"(\s*)"}; + + std::cmatch match; + if (std::regex_search(input.cbegin(), input.cend(), match, ws_regex)) { + return {match[0].first, match[0].length()}; + } + return {}; +} + +} + +} + +class Lexer { +public: + explicit Lexer(const std::string_view& input) + : m_input{input} { + } + + using TokenProcessor = std::function<bool (const lexer::Token&)>; + + bool for_each_token(const TokenProcessor& process) { + parse_token(); + for (auto token = peek_token(); token.has_value(); drop_token(), token = peek_token()) { + if (!process(*token)) { + return false; + } + } + return true; + } + + std::vector<lexer::Token> get_tokens() { + std::vector<lexer::Token> tokens; + for_each_token([&tokens] (const lexer::Token& token) { + tokens.emplace_back(token); + return true; + }); + return tokens; + } + + void parse_token() { + if (m_input.length() == 0) { + return; + } + std::string_view token_view; + m_token_buffer = parse_token(token_view); + if (m_token_buffer.has_value()) { + m_input.remove_prefix(token_view.length()); + } + } + + bool has_token() const { + return peek_token().has_value(); + } + + std::optional<lexer::Token> peek_token() const { + return m_token_buffer; + } + + void drop_token() { + if (!has_token()) { + throw lexer::Error{"internal: no tokens to drop"}; + } + m_token_buffer = {}; + parse_token(); + } + + std::optional<lexer::Token> drop_token_if(lexer::Token::Type type) { + if (!has_token()) { + throw lexer::Error{"internal: no tokens to drop"}; + } + if (m_token_buffer.value().get_type() != type) { + return {}; + } + const auto result = m_token_buffer; + drop_token(); + return result; + } + +private: + void consume_whitespace() { + const auto ws = lexer::details::parse_whitespace(m_input); + m_input.remove_prefix(ws.length()); + } + + std::optional<lexer::Token> parse_token(std::string_view& token_view) { + consume_whitespace(); + if (m_input.length() == 0) { + return {}; + } + if (const auto const_token = lexer::details::parse_const_token(m_input, token_view); const_token.has_value()) { + return lexer::Token{*const_token}; + } + if (const auto number = lexer::details::parse_number(m_input, token_view); number.has_value()) { + return lexer::Token{*number}; + } + throw lexer::Error{"invalid input at: " + std::string{m_input}}; + } + + std::string_view m_input; + std::optional<lexer::Token> m_token_buffer; +}; + +} diff --git a/server/log.hpp b/server/log.hpp new file mode 100644 index 0000000..ca0fafd --- /dev/null +++ b/server/log.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include <boost/format.hpp> +#include <boost/system/error_code.hpp> + +#include <ctime> + +#include <iomanip> +#include <iostream> +#include <sstream> +#include <string> +#include <string_view> +#include <thread> +#include <utility> + +namespace math::server::log { + +namespace details { + +inline std::thread::id get_tid() { return std::this_thread::get_id(); } + +inline std::string get_timestamp() { + const auto tt = std::time(nullptr); + std::ostringstream oss; + oss << std::put_time(std::gmtime(&tt), "%Y-%m-%d %H:%M:%S"); + return oss.str(); +} + +inline void log(const std::string& msg) { + std::clog << get_timestamp() << " | " << get_tid() << " | " << msg << '\n'; +} + +} + +template <typename... Args> +inline void log(const std::string_view& fmt, Args&&... args) { + details::log(boost::str((boost::format(fmt.data()) % ... % args))); +} + +template <typename... Args> +inline void error(const std::string_view& fmt, Args&&... args) { + details::log(boost::str((boost::format(fmt.data()) % ... % args))); +} + +inline void error(const boost::system::error_code& ec) { + details::log(ec.message()); +} + +} diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000..2cf6d35 --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,34 @@ +#include "server.hpp" +#include "settings.hpp" + +#include <boost/program_options.hpp> + +#include <exception> +#include <iostream> + +int main(int argc, char* argv[]) { + try { + math::server::SettingsParser parser{argv[0]}; + + try { + const auto settings = parser.parse(argc, argv); + if (settings.exit_with_usage()) { + parser.usage(); + return 0; + } + + math::server::Server server{settings}; + server.run(); + } catch (const boost::program_options::error& e) { + parser.usage_error(e); + return 1; + } + } catch (const std::exception& e) { + std::cerr << "An error occured: " << e.what() << "\n"; + return 1; + } catch (...) { + std::cerr << "An unknown error occured\n"; + return 1; + } + return 0; +} diff --git a/server/parser.hpp b/server/parser.hpp new file mode 100644 index 0000000..a9e5f54 --- /dev/null +++ b/server/parser.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include "error.hpp" +#include "lexer.hpp" + +#include <optional> +#include <string> +#include <string_view> + +namespace math::server { +namespace parser { + +class Error : public server::Error { +public: + explicit Error(const std::string& what) + : server::Error{"parser error: " + what} + { } +}; + +class BinaryOp { +public: + static bool is(const lexer::Token& token) { + using Type = lexer::Token::Type; + switch (token.get_type()) { + case Type::PLUS: + case Type::MINUS: + case Type::ASTERISK: + case Type::SLASH: + return true; + + default: + return false; + } + } + + static BinaryOp from_token(const lexer::Token& token) { + if (!is(token)) { + throw Error{"internal: token is not a binary operator"}; + } + return BinaryOp{token}; + } + + static constexpr unsigned min_precedence() { return 0; } + + unsigned get_precedence() const { + using Type = lexer::Token::Type; + switch (m_type) { + case Type::PLUS: + case Type::MINUS: + return min_precedence(); + + case Type::ASTERISK: + case Type::SLASH: + return min_precedence() + 1; + + default: + throw Error{"internal: undefined operator precedence"}; + } + } + + double exec(double lhs, double rhs) const { + using Type = lexer::Token::Type; + switch (m_type) { + case Type::PLUS: + return lhs + rhs; + case Type::MINUS: + return lhs - rhs; + case Type::ASTERISK: + return lhs * rhs; + case Type::SLASH: + // Trapping the CPU would be better? + if (rhs == 0.) { + throw Error{"division by zero"}; + } + return lhs / rhs; + default: + throw Error{"internal: unsupported operator"}; + } + } + +private: + explicit BinaryOp(const lexer::Token& token) + : m_type{token.get_type()} + { } + + lexer::Token::Type m_type; +}; + +} + +class Parser { +public: + // I did simple recursive descent parsing a long time ago (see + // https://github.com/egor-tensin/simple-interpreter), this appears to be + // a finer algorithm for parsing arithmetic expressions. + // Reference: https://en.wikipedia.org/wiki/Operator-precedence_parser + + explicit Parser(const std::string_view& input) + : m_lexer{input} + { } + + double exec() { + m_lexer.parse_token(); + const auto result = exec_expr(); + if (m_lexer.has_token()) { + throw parser::Error{"expected a binary operator"}; + } + return result; + } + +private: + double exec_expr() { + return exec_expr(exec_primary(), parser::BinaryOp::min_precedence()); + } + + double exec_expr(double lhs, unsigned min_prec) { + for (auto op = peek_operator(); op.has_value() && op->get_precedence() >= min_prec;) { + const auto lhs_op = *op; + m_lexer.drop_token(); + auto rhs = exec_primary(); + + for (op = peek_operator(); op.has_value() && op->get_precedence() > lhs_op.get_precedence(); op = peek_operator()) { + rhs = exec_expr(rhs, op->get_precedence()); + } + + lhs = lhs_op.exec(lhs, rhs); + } + return lhs; + } + + std::optional<parser::BinaryOp> peek_operator() { + const auto token = m_lexer.peek_token(); + if (!token.has_value() || !parser::BinaryOp::is(*token)) { + return {}; + } + return parser::BinaryOp::from_token(*token); + } + + double exec_primary() { + if (!m_lexer.has_token()) { + throw parser::Error{"expected '-', '(' or a number"}; + } + + using Type = lexer::Token::Type; + + if (m_lexer.drop_token_if(Type::MINUS).has_value()) { + return -exec_primary(); + } + + if (m_lexer.drop_token_if(Type::LEFT_PAREN).has_value()) { + const auto inner = exec_expr(); + if (!m_lexer.has_token() || !m_lexer.drop_token_if(Type::RIGHT_PAREN).has_value()) { + throw parser::Error{"missing closing ')'"}; + } + return inner; + } + + if (const auto token = m_lexer.drop_token_if(Type::NUMBER); token.has_value()) { + return token.value().get_number_value(); + } + + throw parser::Error{"expected '-', '(' or a number"}; + } + + Lexer m_lexer; +}; + +} diff --git a/server/server.cpp b/server/server.cpp new file mode 100644 index 0000000..4dc672c --- /dev/null +++ b/server/server.cpp @@ -0,0 +1,111 @@ +#include "error.hpp" +#include "log.hpp" +#include "server.hpp" +#include "session.hpp" +#include "session_manager.hpp" +#include "settings.hpp" + +#include <boost/asio.hpp> +#include <boost/system/error_code.hpp> +#include <boost/system/system_error.hpp> + +#include <cstddef> + +#include <exception> +#include <thread> +#include <vector> + +namespace math::server { +namespace { + +boost::asio::ip::tcp::endpoint make_endpoint(unsigned port) { + return {boost::asio::ip::tcp::v4(), port}; +} + +void configure_acceptor(boost::asio::ip::tcp::acceptor& acceptor, unsigned port) { + try { + const auto endpoint = make_endpoint(port); + acceptor.open(endpoint.protocol()); + acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true)); + acceptor.bind(endpoint); + acceptor.listen(); + } catch (const boost::system::system_error& e) { + throw Error{e.what()}; + } +} + +} + +Server::Server(const Settings& settings) + : Server{settings.m_port, settings.m_threads} +{ } + +Server::Server(unsigned port, unsigned threads) + : m_numof_threads{threads} + , m_signals{m_io_context} + , m_acceptor{m_io_context} { + + wait_for_signal(); + configure_acceptor(m_acceptor, port); + + accept(); +} + +void Server::run() { + std::vector<std::thread> threads{m_numof_threads}; + for (std::size_t i = 0; i < m_numof_threads; ++i) { + threads[i] = std::thread{[this] () { m_io_context.run(); }}; + } + + for (std::size_t i = 0; i < m_numof_threads; ++i) { + threads[i].join(); + } +} + +void Server::wait_for_signal() { + try { + m_signals.add(SIGINT); + m_signals.add(SIGTERM); + + m_signals.async_wait([this] (const boost::system::error_code& ec, int signo) { + handle_signal(ec, signo); + }); + } catch (const boost::system::system_error& e) { + throw Error{e.what()}; + } +} + +void Server::handle_signal(const boost::system::error_code& ec, int signo) { + if (ec) { + log::error("%1%: %2%", __func__, ec.message()); + } + + log::log("Caught signal %1%", signo); + + try { + m_acceptor.close(); + m_session_mgr.stop_all(); + } catch (const std::exception& e) { + log::error(e.what()); + } +} + +void Server::accept() { + const auto session = m_session_mgr.make_session(m_io_context); + m_acceptor.async_accept(session->socket(), + [session, this] (const boost::system::error_code& ec) { + handle_accept(session, ec); + }); +} + +void Server::handle_accept(SessionPtr session, const boost::system::error_code& ec) { + if (ec) { + log::error("%1%: %2%", __func__, ec.message()); + return; + } + + m_session_mgr.start(session); + accept(); +} + +} diff --git a/server/server.hpp b/server/server.hpp new file mode 100644 index 0000000..5524f88 --- /dev/null +++ b/server/server.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "session_manager.hpp" +#include "settings.hpp" + +#include <boost/asio.hpp> +#include <boost/system/error_code.hpp> + +namespace math::server { + +class Server { +public: + Server(const Settings& settings); + Server(unsigned port, unsigned threads); + + void run(); + +private: + void wait_for_signal(); + void handle_signal(const boost::system::error_code&, int); + + void accept(); + void handle_accept(SessionPtr session, const boost::system::error_code& ec); + + const unsigned m_numof_threads; + + boost::asio::io_context m_io_context; + boost::asio::signal_set m_signals; + boost::asio::ip::tcp::acceptor m_acceptor; + + SessionManager m_session_mgr; +}; + +} diff --git a/server/session.cpp b/server/session.cpp new file mode 100644 index 0000000..409ca5a --- /dev/null +++ b/server/session.cpp @@ -0,0 +1,113 @@ +#include "error.hpp" +#include "log.hpp" +#include "parser.hpp" +#include "session.hpp" +#include "session_manager.hpp" + +#include <boost/asio.hpp> +#include <boost/lexical_cast.hpp> +#include <boost/system/error_code.hpp> +#include <boost/system/system_error.hpp> + +#include <cstddef> + +#include <exception> +#include <string> +#include <utility> + +namespace math::server { +namespace { + +std::string reply_to_string(double result) { + return boost::lexical_cast<std::string>(result); +} + +std::string calc_reply(const std::string& input) { + std::string reply; + try { + reply = reply_to_string(Parser{input}.exec()); + } catch (const std::exception& e) { + reply = e.what(); + } + return reply; +} + +} + +Session::Session(SessionManager& mgr, boost::asio::io_context& io_context) + : m_session_mgr{mgr}, m_strand{io_context}, m_socket{io_context} +{ } + +boost::asio::ip::tcp::socket& Session::socket() { + return m_socket; +} + +void Session::start() { + read(); +} + +void Session::stop() { + close(); +} + +void Session::close() { + try { + m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both); + m_socket.close(); + } catch (const boost::system::system_error& e) { + throw Error{e.what()}; + } +} + +void Session::read() { + const auto self = shared_from_this(); + + // Stop at LF + boost::asio::async_read_until(m_socket, m_buffer, '\n', boost::asio::bind_executor(m_strand, + [this, self] (const boost::system::error_code& ec, std::size_t bytes) { + handle_read(ec, bytes); + })); +} + +void Session::handle_read(const boost::system::error_code& ec, std::size_t bytes) { + if (ec) { + log::error("%1%: %2%", __func__, ec.message()); + m_session_mgr.stop(shared_from_this()); + return; + } + + write(calc_reply(consume_input(bytes))); +} + +std::string Session::consume_input(std::size_t bytes) { + const auto data = boost::asio::buffer_cast<const char*>(m_buffer.data()); + const std::string input{data, bytes - 1}; + m_buffer.consume(bytes); + return input; +} + +void Session::write(std::string output) { + const auto self = shared_from_this(); + + // Include CR (so that Windows' telnet client works) + output += "\r\n"; + + boost::asio::const_buffer buffer{output.c_str(), output.length()}; + + boost::asio::async_write(m_socket, std::move(buffer), boost::asio::bind_executor(m_strand, + [this, self] (const boost::system::error_code& ec, std::size_t bytes) { + handle_write(ec, bytes); + })); +} + +void Session::handle_write(const boost::system::error_code& ec, std::size_t bytes) { + if (ec) { + log::error("%1%: %2%", __func__, ec.message()); + m_session_mgr.stop(shared_from_this()); + return; + } + + read(); +} + +} diff --git a/server/session.hpp b/server/session.hpp new file mode 100644 index 0000000..ace3755 --- /dev/null +++ b/server/session.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <boost/asio.hpp> +#include <boost/system/error_code.hpp> + +#include <cstddef> + +#include <memory> +#include <string> + +namespace math::server { + +class SessionManager; + +class Session : public std::enable_shared_from_this<Session> { +public: + Session(SessionManager& mgr, boost::asio::io_context& io_context); + + boost::asio::ip::tcp::socket& socket(); + + void start(); + void stop(); + +private: + void close(); + + void read(); + void write(std::string); + + void handle_read(const boost::system::error_code&, std::size_t); + void handle_write(const boost::system::error_code&, std::size_t); + + std::string consume_input(std::size_t); + + SessionManager& m_session_mgr; + + boost::asio::io_context::strand m_strand; + boost::asio::ip::tcp::socket m_socket; + boost::asio::streambuf m_buffer; +}; + +} diff --git a/server/session_manager.cpp b/server/session_manager.cpp new file mode 100644 index 0000000..a42fca4 --- /dev/null +++ b/server/session_manager.cpp @@ -0,0 +1,37 @@ +#include "log.hpp" +#include "session.hpp" +#include "session_manager.hpp" + +#include <memory> +#include <mutex> + +namespace math::server { + +SessionPtr SessionManager::make_session(boost::asio::io_context& io_context) { + return std::make_shared<Session>(*this, io_context); +} + +void SessionManager::start(const SessionPtr& session) { + std::lock_guard<std::mutex> lck{m_mtx}; + m_sessions.emplace(session); + session->start(); +} + +void SessionManager::stop(const SessionPtr& session) { + std::lock_guard<std::mutex> lck{m_mtx}; + const auto removed = m_sessions.erase(session) > 0; + if (removed) { + session->stop(); + } +} + +void SessionManager::stop_all() { + std::lock_guard<std::mutex> lck{m_mtx}; + log::log("Closing the remaining %1% session(s)...", m_sessions.size()); + for (const auto& session : m_sessions) { + session->stop(); + } + m_sessions.clear(); +} + +} diff --git a/server/session_manager.hpp b/server/session_manager.hpp new file mode 100644 index 0000000..f0bec0b --- /dev/null +++ b/server/session_manager.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include <boost/asio.hpp> + +#include <mutex> +#include <memory> +#include <unordered_set> + +namespace math::server { + +class Session; +using SessionPtr = std::shared_ptr<Session>; + +class SessionManager { +public: + SessionManager() = default; + + SessionPtr make_session(boost::asio::io_context&); + + void start(const SessionPtr&); + void stop(const SessionPtr&); + + void stop_all(); + +private: + std::mutex m_mtx; + std::unordered_set<SessionPtr> m_sessions; +}; + +} diff --git a/server/settings.hpp b/server/settings.hpp new file mode 100644 index 0000000..310163f --- /dev/null +++ b/server/settings.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include <boost/filesystem.hpp> +#include <boost/program_options.hpp> + +#include <exception> +#include <iostream> +#include <string> +#include <thread> +#include <vector> + +namespace math::server { + +struct Settings { + static constexpr unsigned DEFAULT_PORT = 18000; + + static unsigned default_threads() { return std::thread::hardware_concurrency(); } + + unsigned m_port; + unsigned m_threads; + + bool exit_with_usage() const { return m_vm.count("help"); } + + boost::program_options::variables_map m_vm; +}; + +class SettingsParser { +public: + explicit SettingsParser(const std::string& argv0) + : m_prog_name{extract_filename(argv0)} + { + m_visible.add_options() + ("help,h", + "show this message and exit") + ("port,p", + boost::program_options::value(&m_settings.m_port)->default_value(Settings::DEFAULT_PORT), + "server port number") + ("threads,n", + boost::program_options::value(&m_settings.m_threads)->default_value(Settings::default_threads()), + "number of threads"); + } + + static const char* get_short_description() { + return "[-h|--help] [-p|--port] [-n|--threads]"; + } + + Settings parse(int argc, char* argv[]) { + boost::program_options::store( + boost::program_options::command_line_parser{argc, argv} + .options(m_visible) + .run(), + m_settings.m_vm); + if (m_settings.exit_with_usage()) { + return m_settings; + } + boost::program_options::notify(m_settings.m_vm); + return m_settings; + } + + void usage() const { + std::cout << *this; + } + + void usage_error(const std::exception& e) const { + std::cerr << "usage error: " << e.what() << '\n'; + std::cerr << *this; + } + +private: + static std::string extract_filename(const std::string& path) { + return boost::filesystem::path{path}.filename().string(); + } + + const std::string m_prog_name; + + boost::program_options::options_description m_visible; + + Settings m_settings; + + friend std::ostream& operator<<(std::ostream& os, const SettingsParser& parser) { + os << "usage: " << parser.m_prog_name << ' ' << get_short_description() << '\n'; + os << parser.m_visible; + return os; + } +}; + +} |