From 90bd600c5025ede4db99122f13dfb07b27de46ae Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 30 Nov 2019 01:38:08 +0300 Subject: initial commit --- .gitattributes | 1 + CMakeLists.txt | 11 ++ client/CMakeLists.txt | 5 + client/client.hpp | 52 ++++++++ client/error.hpp | 15 +++ client/input.hpp | 139 ++++++++++++++++++++++ client/main.cpp | 34 ++++++ client/settings.hpp | 111 ++++++++++++++++++ client/transport.hpp | 104 ++++++++++++++++ common.cmake | 107 +++++++++++++++++ server/CMakeLists.txt | 11 ++ server/error.hpp | 15 +++ server/lexer.hpp | 261 +++++++++++++++++++++++++++++++++++++++++ server/log.hpp | 49 ++++++++ server/main.cpp | 34 ++++++ server/parser.hpp | 168 ++++++++++++++++++++++++++ server/server.cpp | 111 ++++++++++++++++++ server/server.hpp | 34 ++++++ server/session.cpp | 113 ++++++++++++++++++ server/session.hpp | 42 +++++++ server/session_manager.cpp | 37 ++++++ server/session_manager.hpp | 30 +++++ server/settings.hpp | 87 ++++++++++++++ test/CMakeLists.txt | 1 + test/unit_tests/CMakeLists.txt | 6 + test/unit_tests/lexer.cpp | 109 +++++++++++++++++ test/unit_tests/main.cpp | 2 + test/unit_tests/parser.cpp | 48 ++++++++ 28 files changed, 1737 insertions(+) create mode 100644 .gitattributes create mode 100644 CMakeLists.txt create mode 100644 client/CMakeLists.txt create mode 100644 client/client.hpp create mode 100644 client/error.hpp create mode 100644 client/input.hpp create mode 100644 client/main.cpp create mode 100644 client/settings.hpp create mode 100644 client/transport.hpp create mode 100644 common.cmake create mode 100644 server/CMakeLists.txt create mode 100644 server/error.hpp create mode 100644 server/lexer.hpp create mode 100644 server/log.hpp create mode 100644 server/main.cpp create mode 100644 server/parser.hpp create mode 100644 server/server.cpp create mode 100644 server/server.hpp create mode 100644 server/session.cpp create mode 100644 server/session.hpp create mode 100644 server/session_manager.cpp create mode 100644 server/session_manager.hpp create mode 100644 server/settings.hpp create mode 100644 test/CMakeLists.txt create mode 100644 test/unit_tests/CMakeLists.txt create mode 100644 test/unit_tests/lexer.cpp create mode 100644 test/unit_tests/main.cpp create mode 100644 test/unit_tests/parser.cpp diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0d02300 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +project(math_server CXX) + +option(ENABLE_TESTS "build the tests") + +include(common.cmake) + +add_subdirectory(client) +add_subdirectory(server) +if(ENABLE_TESTS) + add_subdirectory(test) +endif() diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..cd23de8 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,5 @@ +find_package(Boost REQUIRED COMPONENTS filesystem program_options) + +add_executable(client main.cpp) +target_include_directories(client SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) +target_link_libraries(client PRIVATE ${Boost_LIBRARIES}) diff --git a/client/client.hpp b/client/client.hpp new file mode 100644 index 0000000..139ebb2 --- /dev/null +++ b/client/client.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "input.hpp" +#include "settings.hpp" +#include "transport.hpp" + +#include +#include +#include + +namespace math::client { + +class Client { +public: + explicit Client(const Settings& settings) + : Client{make_input_reader(settings), make_transport(settings)} + { } + + Client(input::ReaderPtr&& input_reader, TransportPtr&& transport) + : m_input_reader{std::move(input_reader)} + , m_transport{std::move(transport)} + { } + + void run() { + m_input_reader->for_each_input([this] (const std::string& input) { + m_transport->send_query(input, [] (const std::string& reply) { + std::cout << reply << '\n'; + }); + return true; + }); + } + +private: + static input::ReaderPtr make_input_reader(const Settings& settings) { + if (settings.input_from_string()) { + return input::make_string_reader(settings.m_input); + } + if (settings.input_from_files()) { + return input::make_file_reader(settings.m_files); + } + return input::make_console_reader(); + } + + static TransportPtr make_transport(const Settings& settings) { + return make_blocking_network_transport(settings.m_host, settings.m_port); + } + + const input::ReaderPtr m_input_reader; + TransportPtr m_transport; +}; + +} diff --git a/client/error.hpp b/client/error.hpp new file mode 100644 index 0000000..98e1305 --- /dev/null +++ b/client/error.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace math::client { + +class Error : public std::runtime_error { +public: + explicit Error(const std::string& what) + : std::runtime_error{"client error: " + what} + { } +}; + +} diff --git a/client/input.hpp b/client/input.hpp new file mode 100644 index 0000000..077d5c7 --- /dev/null +++ b/client/input.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include "error.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace math::client::input { + +class Error : public client::Error { +public: + explicit Error(const std::string& what) + : client::Error{"input error: " + what} { + } +}; + +class Reader { +public: + using InputHandler = std::function; + + virtual ~Reader() = default; + + virtual bool for_each_input(const InputHandler& process) const = 0; +}; + +using ReaderPtr = std::unique_ptr; + +class FileReader : public Reader { +public: + explicit FileReader(const std::string& path) + : m_path{path} + { } + + bool for_each_input(const InputHandler& process) const override { + return enum_lines(process); + } + +private: + bool enum_lines(const InputHandler& process) const { + std::ifstream ifs; + ifs.exceptions(std::ifstream::badbit); + + try { + ifs.open(m_path); + if (!ifs.is_open()) { + throw Error{"couldn't open file: " + m_path}; + } + + for (std::string line; std::getline(ifs, line);) { + if (!process(line)) { + return false; + } + } + } catch (const std::exception& e) { + throw Error{e.what()}; + } + + return true; + } + + const std::string m_path; +}; + +class MultiFileReader : public Reader { +public: + explicit MultiFileReader(const std::vector& paths) + : m_paths{paths} + { } + + bool for_each_input(const InputHandler& process) const override { + for (const auto& path : m_paths) { + const FileReader reader{path}; + if (!reader.for_each_input(process)) { + return false; + } + } + return true; + } + +private: + const std::vector m_paths; +}; + +inline input::ReaderPtr make_file_reader(const std::string& path) { + return std::make_unique(path); +} + +inline input::ReaderPtr make_file_reader(const std::vector& paths) { + return std::make_unique(paths); +} + +class StringReader : public Reader { +public: + explicit StringReader(const std::string& input) + : m_input{input} + { } + + bool for_each_input(const InputHandler& process) const override { + return process(m_input); + } + +private: + const std::string m_input; +}; + +inline input::ReaderPtr make_string_reader(const std::string& input) { + return std::make_unique(input); +} + +class ConsoleReader : public Reader { +public: + ConsoleReader() = default; + + bool for_each_input(const InputHandler& process) const override { + std::string line; + while (read_line(line)) { + if (!process(line)) { + return false; + } + } + return true; + } + +private: + static bool read_line(std::string& dest) { + return static_cast(std::getline(std::cin, dest)); + } +}; + +inline input::ReaderPtr make_console_reader() { + return std::make_unique(); +} + +} diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..cfe5026 --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,34 @@ +#include "client.hpp" +#include "settings.hpp" + +#include + +#include +#include + +int main(int argc, char* argv[]) { + try { + math::client::SettingsParser parser{argv[0]}; + + try { + const auto settings = parser.parse(argc, argv); + if (settings.exit_with_usage()) { + parser.usage(); + return 0; + } + + math::client::Client client{settings}; + client.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/client/settings.hpp b/client/settings.hpp new file mode 100644 index 0000000..5c2522e --- /dev/null +++ b/client/settings.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include "transport.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace math::client { + +struct Settings { + std::string m_input; + std::string m_host; + std::string m_port; + std::vector m_files; + + bool exit_with_usage() const { return m_vm.count("help"); } + + bool input_from_string() const { + return m_vm.count("command"); + } + + bool input_from_files() const { + return !input_from_string() && !m_files.empty(); + } + + bool input_from_console() const { + return !input_from_string() && !input_from_files(); + } + + 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") + ("command,c", + boost::program_options::value(&m_settings.m_input), + "evaluate the argument expression and exit") + ("host,H", + boost::program_options::value(&m_settings.m_host)->default_value("localhost"), + "server host address") + ("port,p", + boost::program_options::value(&m_settings.m_port)->default_value(NetworkTransport::DEFAULT_PORT), + "server port number"); + m_hidden.add_options() + ("files", + boost::program_options::value>(&m_settings.m_files), + "shouldn't be visible"); + m_positional.add("files", -1); + } + + static const char* get_short_description() { + return "[-h|--help] [-c|--command arg] [-H|--host] [-p|--port] [file...]"; + } + + Settings parse(int argc, char* argv[]) { + boost::program_options::options_description all; + all.add(m_hidden).add(m_visible); + boost::program_options::store( + boost::program_options::command_line_parser{argc, argv} + .options(all) + .positional(m_positional) + .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_hidden; + boost::program_options::options_description m_visible; + boost::program_options::positional_options_description m_positional; + + 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; + } +}; + +} diff --git a/client/transport.hpp b/client/transport.hpp new file mode 100644 index 0000000..ae56f73 --- /dev/null +++ b/client/transport.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include "error.hpp" + +#include +#include + +#include +#include +#include + +namespace math::client { +namespace transport { + +class Error : public client::Error { +public: + explicit Error(const std::string& msg) + : client::Error{"transport error: " + msg} + { } +}; + +} + +class Transport { +public: + virtual ~Transport() = default; + + using ProcessResult = std::function; + + virtual void send_query(const std::string&, const ProcessResult&) = 0; +}; + +using TransportPtr = std::unique_ptr; + +class NetworkTransport : public Transport { +public: + static constexpr auto DEFAULT_PORT = "18000"; + + NetworkTransport(const std::string& host, const std::string& port) + : m_host{host}, m_port{port} + { } + +protected: + const std::string m_host; + const std::string m_port; +}; + +class BlockingNetworkTransport : public NetworkTransport { +public: + BlockingNetworkTransport(const std::string &host, const std::string& port) + : NetworkTransport{host, port}, m_socket{m_io_context} { + try { + connect(); + } catch (const boost::system::system_error& e) { + throw transport::Error{e.what()}; + } + } + + void send_query(const std::string& query, const ProcessResult& on_reply) override { + std::string reply; + try { + reply = send_query(query); + } catch (const boost::system::system_error& e) { + throw transport::Error{e.what()}; + } + on_reply(reply); + } + +private: + void connect() { + boost::asio::ip::tcp::resolver resolver{m_io_context}; + boost::asio::connect(m_socket, resolver.resolve(m_host, m_port)); + } + + std::string send_query(const std::string& query) { + write(query); + return read_line(); + } + + void write(std::string input) { + input += '\n'; + boost::asio::write(m_socket, boost::asio::const_buffer{input.c_str(), input.size()}); + } + + std::string read_line() { + const auto bytes = boost::asio::read_until(m_socket, m_buffer, '\n'); + const auto data = boost::asio::buffer_cast(m_buffer.data()); + const std::string result{data, bytes - 1}; + m_buffer.consume(bytes); + return result; + } + + boost::asio::io_context m_io_context; + boost::asio::ip::tcp::socket m_socket; + boost::asio::streambuf m_buffer; +}; + +inline TransportPtr make_blocking_network_transport( + const std::string& host, const std::string& port) { + + return std::make_unique(host, port); +} + +} diff --git a/common.cmake b/common.cmake new file mode 100644 index 0000000..2d5e9f9 --- /dev/null +++ b/common.cmake @@ -0,0 +1,107 @@ +# Copyright (c) 2017 Egor Tensin +# It's a CMake code snippet I use in all of my CMake projects. +# It makes targets link the runtime statically by default + strips debug +# symbols in release builds. +# The latest version can be found at +# https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +# Version: 2019-11-29T22:30:28Z + +get_directory_property(parent_directory PARENT_DIRECTORY) +set(is_root_project $) + +if(CMAKE_C_COMPILER) + set(toolset "${CMAKE_C_COMPILER_ID}") +elseif(CMAKE_CXX_COMPILER) + set(toolset "${CMAKE_CXX_COMPILER_ID}") +else() + set(toolset "unknown") +endif() + +set(USE_STATIC_RUNTIME "${is_root_project}" CACHE BOOL "Link the runtime statically") +set(STRIP_SYMBOL_TABLE "${is_root_project}" CACHE BOOL "Strip symbol tables") +set(CXX_STANDARD "17" CACHE BOOL "Set C++ standard version") + +if(is_root_project) + if(MSVC) + add_compile_options(/MP /W4) + elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) + add_compile_options(-Wall -Wextra) + else() + message(WARNING "Unrecognized toolset: ${toolset}") + endif() +endif() + +set(CMAKE_CXX_STANDARD "${CXX_STANDARD}") +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +function(use_static_runtime_msvc target) + get_target_property(target_type "${target}" TYPE) + if(target_type STREQUAL INTERFACE_LIBRARY) + else() + target_compile_options("${target}" PRIVATE + $<$:/MTd> + $<$>:/MT>) + endif() +endfunction() + +function(use_static_runtime_gcc target) + get_target_property(target_type "${target}" TYPE) + if(target_type STREQUAL EXECUTABLE) + target_link_libraries("${target}" PRIVATE -static) + endif() +endfunction() + +function(use_static_runtime target) + if(MSVC) + use_static_runtime_msvc("${target}") + elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) + use_static_runtime_gcc("${target}") + else() + message(WARNING "Unrecognized toolset: ${toolset}") + endif() +endfunction() + +function(strip_symbol_table_gcc target) + get_target_property(target_type "${target}" TYPE) + set(release_build $,$>) + if(target_type STREQUAL INTERFACE_LIBRARY) + else() + target_link_libraries("${target}" PRIVATE $<${release_build}:-s>) + endif() +endfunction() + +function(strip_symbol_table target) + if(MSVC) + elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) + strip_symbol_table_gcc("${target}") + else() + message(WARNING "Unrecognized toolset: ${toolset}") + endif() +endfunction() + +function(apply_common_settings target) + if(TARGET "${target}") + get_target_property(target_imported "${target}" IMPORTED) + if(target_imported STREQUAL NOTFOUND OR NOT target_imported) + if(STRIP_SYMBOL_TABLE) + strip_symbol_table("${target}") + endif() + if(USE_STATIC_RUNTIME) + use_static_runtime("${target}") + endif() + endif() + endif() +endfunction() + +macro(add_executable target) + _add_executable(${ARGV}) + apply_common_settings("${target}") +endmacro() + +macro(add_library target) + _add_library(${ARGV}) + apply_common_settings("${target}") +endmacro() 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 +#include + +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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::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 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 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 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 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 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 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 get_tokens() { + std::vector 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 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 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 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 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 +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +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 +inline void log(const std::string_view& fmt, Args&&... args) { + details::log(boost::str((boost::format(fmt.data()) % ... % args))); +} + +template +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 + +#include +#include + +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 +#include +#include + +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 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 +#include +#include + +#include + +#include +#include +#include + +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 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 +#include + +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 +#include +#include +#include + +#include + +#include +#include +#include + +namespace math::server { +namespace { + +std::string reply_to_string(double result) { + return boost::lexical_cast(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(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 +#include + +#include + +#include +#include + +namespace math::server { + +class SessionManager; + +class Session : public std::enable_shared_from_this { +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 +#include + +namespace math::server { + +SessionPtr SessionManager::make_session(boost::asio::io_context& io_context) { + return std::make_shared(*this, io_context); +} + +void SessionManager::start(const SessionPtr& session) { + std::lock_guard lck{m_mtx}; + m_sessions.emplace(session); + session->start(); +} + +void SessionManager::stop(const SessionPtr& session) { + std::lock_guard lck{m_mtx}; + const auto removed = m_sessions.erase(session) > 0; + if (removed) { + session->stop(); + } +} + +void SessionManager::stop_all() { + std::lock_guard 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 + +#include +#include +#include + +namespace math::server { + +class Session; +using SessionPtr = std::shared_ptr; + +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 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 +#include + +#include +#include +#include +#include +#include + +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; + } +}; + +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..bed23ce --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(unit_tests) diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt new file mode 100644 index 0000000..d320974 --- /dev/null +++ b/test/unit_tests/CMakeLists.txt @@ -0,0 +1,6 @@ +find_package(Boost REQUIRED) + +add_executable(unit_tests main.cpp lexer.cpp parser.cpp) +target_include_directories(unit_tests SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) + +target_include_directories(unit_tests PRIVATE ../..) diff --git a/test/unit_tests/lexer.cpp b/test/unit_tests/lexer.cpp new file mode 100644 index 0000000..7b513e8 --- /dev/null +++ b/test/unit_tests/lexer.cpp @@ -0,0 +1,109 @@ +#include + +#include + +#include + +BOOST_AUTO_TEST_CASE(test_lexer_parse_number) { + using namespace math::server::lexer; + + // These are valid numbers: + BOOST_TEST(details::parse_number("0").value() == 0); + BOOST_TEST(details::parse_number("1.").value() == 1.); + BOOST_TEST(details::parse_number(".1").value() == .1); + // parse_* functions only consume a single token: + BOOST_TEST(details::parse_number(".1+1").value() == .1); + BOOST_TEST(details::parse_number("1e3").value() == 1e3); + BOOST_TEST(details::parse_number(".123e1").value() == .123e1); + BOOST_TEST(details::parse_number("123e-1").value() == 123e-1); + BOOST_TEST(details::parse_number("123e+1").value() == 123e+1); + BOOST_TEST(details::parse_number("1.e6").value() == 1.e6); + BOOST_TEST(details::parse_number("2.3e-4").value() == 2.3e-4); + // These are not numbers, but perhaps something else? + BOOST_TEST(!details::parse_number(".").has_value()); + BOOST_TEST(!details::parse_number(".e3").has_value()); + BOOST_TEST(!details::parse_number("e12").has_value()); + // This is definitely a number, but a malformed one (an exponent must be + // followed by some digits). + BOOST_CHECK_THROW(details::parse_number("12e"), Error); +} + +BOOST_AUTO_TEST_CASE(test_lexer_parse_const_token) { + using namespace math::server::lexer; + + // TODO: No time to implement the required string conversions, hence the + // extra parentheses. + BOOST_TEST((details::parse_const_token("+").value() == Token::Type::PLUS)); + // parse_* functions only consume a single token: + BOOST_TEST((details::parse_const_token("+++").value() == Token::Type::PLUS)); + BOOST_TEST((details::parse_const_token("-").value() == Token::Type::MINUS)); + BOOST_TEST(!details::parse_const_token("&+").has_value()); +} + +BOOST_AUTO_TEST_CASE(test_lexer_get_tokens) { + using namespace math::server; + using namespace math::server::lexer; + + // TODO: No time to implement the required string conversions, hence the + // extra parentheses. + { + Lexer lexer{""}; + BOOST_TEST((lexer.get_tokens() == std::vector{})); + } + { + Lexer lexer{" + - "}; + BOOST_TEST((lexer.get_tokens() == std::vector{ + Token{Token::Type::PLUS}, + Token{Token::Type::MINUS}, + })); + } + { + Lexer lexer{"&"}; + BOOST_CHECK_THROW((lexer.get_tokens()), lexer::Error); + } + { + Lexer lexer{" 1 + 123 & 456"}; + BOOST_CHECK_THROW((lexer.get_tokens()), lexer::Error); + } + { + Lexer lexer{"1+2"}; + BOOST_TEST((lexer.get_tokens() == std::vector{ + Token{1}, + Token{Token::Type::PLUS}, + Token{2}, + })); + } + { + Lexer lexer{"1+2 * (3- 4e-2)"}; + BOOST_TEST((lexer.get_tokens() == std::vector{ + Token{1}, + Token{Token::Type::PLUS}, + Token{2}, + Token{Token::Type::ASTERISK}, + Token{Token::Type::LEFT_PAREN}, + Token{3}, + Token{Token::Type::MINUS}, + Token{4e-2}, + Token{Token::Type::RIGHT_PAREN}, + })); + } + { + Lexer lexer{" 2 * (1 + 3 * (1 - -3)) "}; + BOOST_TEST((lexer.get_tokens() == std::vector{ + Token{2}, + Token{Token::Type::ASTERISK}, + Token{Token::Type::LEFT_PAREN}, + Token{1}, + Token{Token::Type::PLUS}, + Token{3}, + Token{Token::Type::ASTERISK}, + Token{Token::Type::LEFT_PAREN}, + Token{1}, + Token{Token::Type::MINUS}, + Token{Token::Type::MINUS}, + Token{3}, + Token{Token::Type::RIGHT_PAREN}, + Token{Token::Type::RIGHT_PAREN}, + })); + } +} diff --git a/test/unit_tests/main.cpp b/test/unit_tests/main.cpp new file mode 100644 index 0000000..5d8b492 --- /dev/null +++ b/test/unit_tests/main.cpp @@ -0,0 +1,2 @@ +#define BOOST_TEST_MODULE math_server tests +#include diff --git a/test/unit_tests/parser.cpp b/test/unit_tests/parser.cpp new file mode 100644 index 0000000..11f48d3 --- /dev/null +++ b/test/unit_tests/parser.cpp @@ -0,0 +1,48 @@ +#include + +#include + +BOOST_AUTO_TEST_CASE(test_parser_exec) { + using namespace math::server; + + { + Parser parser{""}; + BOOST_CHECK_THROW(parser.exec(), parser::Error); + } + { + Parser parser{"1"}; + BOOST_TEST(parser.exec() == 1); + } + { + Parser parser{" 1 + "}; + BOOST_CHECK_THROW(parser.exec(), parser::Error); + } + { + Parser parser{" 1 + 2 "}; + BOOST_TEST(parser.exec() == 3); + } + { + Parser parser{" 2 * 1 + 3 "}; + BOOST_TEST(parser.exec() == 5); + } + { + Parser parser{" 2 * (1 + 3) "}; + BOOST_TEST(parser.exec() == 8); + } + { + Parser parser{" 2 * (1 + 3 "}; + BOOST_CHECK_THROW(parser.exec(), parser::Error); + } + { + Parser parser{" 2 * (1 + 3) )"}; + BOOST_CHECK_THROW(parser.exec(), parser::Error); + } + { + Parser parser{" 2 * (1 + 3 * (1 - -3)) "}; + BOOST_TEST(parser.exec() == 26); + } + { + Parser parser{" -2 * ---- (3 + -100e-1) "}; // Looks weird, but also works in e.g. Python + BOOST_TEST(parser.exec() == 14); + } +} -- cgit v1.2.3