From eae27633db83b119f5abac74d41350204a55a9e7 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Fri, 26 Nov 2021 19:04:21 +0100 Subject: [PATCH] Add commandline arguments, implement --config, --version, --help --- CMakeLists.txt | 3 +- include/ArgsParser.h | 49 ++++++++++++++++++++++ include/TConfig.h | 3 +- include/TConsole.h | 1 + include/TServer.h | 2 +- src/ArgsParser.cpp | 94 ++++++++++++++++++++++++++++++++++++++++++ src/TConfig.cpp | 37 ++++++++--------- src/TServer.cpp | 14 +++---- src/main.cpp | 98 +++++++++++++++++++++++++++++++++++++++++--- 9 files changed, 266 insertions(+), 35 deletions(-) create mode 100644 include/ArgsParser.h create mode 100644 src/ArgsParser.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 12523de..0a50caa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,7 +97,8 @@ add_executable(BeamMP-Server include/TNetwork.h src/TNetwork.cpp include/LuaAPI.h src/LuaAPI.cpp include/TScopedTimer.h src/TScopedTimer.cpp - include/SignalHandling.h src/SignalHandling.cpp) + include/SignalHandling.h src/SignalHandling.cpp + include/ArgsParser.h src/ArgsParser.cpp) target_compile_definitions(BeamMP-Server PRIVATE SECRET_SENTRY_URL="${BEAMMP_SECRET_SENTRY_URL}") include_directories(BeamMP-Server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/include/ArgsParser.h b/include/ArgsParser.h new file mode 100644 index 0000000..c951849 --- /dev/null +++ b/include/ArgsParser.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +/* + * Allows syntax: + * --help : long flags + * --path=/home/lion : long assignments + */ +class ArgsParser { +public: + enum Flags : int { + NONE = 0, + REQUIRED = 1, // argument is required + HAS_VALUE = 2, // argument must have a value + }; + + ArgsParser() = default; + + void Parse(const std::vector& ArgList); + // prints errors if any errors occurred, in that case also returns false + bool Verify(); + void RegisterArgument(std::vector&& ArgumentNames, int Flags); + // pass all possible names for this argument (short, long, etc) + bool FoundArgument(const std::vector& Names); + std::optional GetValueOfArgument(const std::vector& Names); + +private: + void ConsumeLongAssignment(const std::string& Arg); + void ConsumeLongFlag(const std::string& Arg); + bool IsRegistered(const std::string& Name); + + struct Argument { + std::string Name; + std::optional Value; + }; + + struct RegisteredArgument { + std::vector Names; + int Flags; + }; + + std::vector mRegisteredArguments; + std::vector mFoundArgs; +}; diff --git a/include/TConfig.h b/include/TConfig.h index 1399aa6..f20766f 100644 --- a/include/TConfig.h +++ b/include/TConfig.h @@ -8,7 +8,7 @@ namespace fs = std::filesystem; class TConfig { public: - explicit TConfig(); + explicit TConfig(const std::string& ConfigFileName); [[nodiscard]] bool Failed() const { return mFailed; } @@ -22,4 +22,5 @@ private: void ParseOldFormat(); bool mFailed { false }; + std::string mConfigFileName; }; diff --git a/include/TConsole.h b/include/TConsole.h index 4f9cad6..053deaa 100644 --- a/include/TConsole.h +++ b/include/TConsole.h @@ -15,6 +15,7 @@ public: void WriteRaw(const std::string& str); void InitializeLuaConsole(TLuaEngine& Engine); void BackupOldLog(); + Commandline& Internal() { return mCommandline; } private: Commandline mCommandline; diff --git a/include/TServer.h b/include/TServer.h index 2f203d8..dded2d9 100644 --- a/include/TServer.h +++ b/include/TServer.h @@ -16,7 +16,7 @@ class TServer final { public: using TClientSet = std::unordered_set>; - TServer(int argc, char** argv); + TServer(const std::vector& Arguments); void InsertClient(const std::shared_ptr& Ptr); std::weak_ptr InsertNewClient(); diff --git a/src/ArgsParser.cpp b/src/ArgsParser.cpp new file mode 100644 index 0000000..8440661 --- /dev/null +++ b/src/ArgsParser.cpp @@ -0,0 +1,94 @@ +#include "ArgsParser.h" +#include "Common.h" +#include + +void ArgsParser::Parse(const std::vector& ArgList) { + for (const auto& Arg : ArgList) { + if (Arg.size() > 2 && Arg.substr(0, 2) == "--") { + // long arg + if (Arg.find("=") != Arg.npos) { + ConsumeLongAssignment(std::string(Arg)); + } else { + ConsumeLongFlag(std::string(Arg)); + } + } else { + beammp_error("Error parsing commandline arguments: Supplied argument '" + std::string(Arg) + "' is not a valid argument and was ignored."); + } + } +} + +bool ArgsParser::Verify() { + bool Ok = true; + for (const auto& RegisteredArg : mRegisteredArguments) { + if (RegisteredArg.Flags & Flags::REQUIRED && !FoundArgument(RegisteredArg.Names)) { + beammp_error("Error in commandline arguments: Argument '" + std::string(RegisteredArg.Names.at(0)) + "' is required but wasn't found."); + Ok = false; + continue; + } else if (FoundArgument(RegisteredArg.Names)) { + if (RegisteredArg.Flags & Flags::HAS_VALUE) { + if (!GetValueOfArgument(RegisteredArg.Names).has_value()) { + beammp_error("Error in commandline arguments: Argument '" + std::string(RegisteredArg.Names.at(0)) + "' expects a value, but no value was given."); + Ok = false; + } + } else if (GetValueOfArgument(RegisteredArg.Names).has_value()) { + beammp_error("Error in commandline arguments: Argument '" + std::string(RegisteredArg.Names.at(0)) + "' does not expect a value, but one was given."); + Ok = false; + } + } + } + return Ok; +} + +void ArgsParser::RegisterArgument(std::vector&& ArgumentNames, int Flags) { + mRegisteredArguments.push_back({ ArgumentNames, Flags }); +} + +bool ArgsParser::FoundArgument(const std::vector& Names) { + // if any of the found args match any of the names + return std::any_of(mFoundArgs.begin(), mFoundArgs.end(), + [&Names](const Argument& Arg) -> bool { + // if any of the names match this arg's name + return std::any_of(Names.begin(), Names.end(), [&Arg](const std::string& Name) -> bool { + return Arg.Name == Name; + }); + }); +} + +std::optional ArgsParser::GetValueOfArgument(const std::vector& Names) { + // finds an entry which has a name that is any of the names in 'Names' + auto Found = std::find_if(mFoundArgs.begin(), mFoundArgs.end(), [&Names](const Argument& Arg) -> bool { + return std::any_of(Names.begin(), Names.end(), [&Arg](const std::string_view& Name) -> bool { + return Arg.Name == Name; + }); + }); + if (Found != mFoundArgs.end()) { + // found + return Found->Value; + } else { + return std::nullopt; + } +} + +bool ArgsParser::IsRegistered(const std::string& Name) { + return std::any_of(mRegisteredArguments.begin(), mRegisteredArguments.end(), [&Name](const RegisteredArgument& Arg) { + auto Iter = std::find(Arg.Names.begin(), Arg.Names.end(), Name); + return Iter != Arg.Names.end(); + }); +} + +void ArgsParser::ConsumeLongAssignment(const std::string& Arg) { + auto Value = Arg.substr(Arg.rfind("=") + 1); + auto Name = Arg.substr(2, Arg.rfind("=") - 2); + if (!IsRegistered(Name)) { + beammp_warn("Argument '" + Name + "' was supplied but isn't a known argument, so it is likely being ignored."); + } + mFoundArgs.push_back({ Name, Value }); +} + +void ArgsParser::ConsumeLongFlag(const std::string& Arg) { + auto Name = Arg.substr(2, Arg.rfind("=") - 2); + mFoundArgs.push_back({ Name, std::nullopt }); + if (!IsRegistered(Name)) { + beammp_warn("Argument '" + Name + "' was supplied but isn't a known argument, so it is likely being ignored."); + } +} diff --git a/src/TConfig.cpp b/src/TConfig.cpp index 8e014f5..a6f20c2 100644 --- a/src/TConfig.cpp +++ b/src/TConfig.cpp @@ -8,8 +8,6 @@ #include #include -static const char* ConfigFileName = static_cast("ServerConfig.toml"); - static constexpr std::string_view StrDebug = "Debug"; static constexpr std::string_view StrPrivate = "Private"; static constexpr std::string_view StrPort = "Port"; @@ -23,19 +21,6 @@ static constexpr std::string_view StrAuthKey = "AuthKey"; static constexpr std::string_view StrSendErrors = "SendErrors"; static constexpr std::string_view StrSendErrorsMessageEnabled = "SendErrorsShowMessage"; -TConfig::TConfig() { - if (!fs::exists(ConfigFileName) || !fs::is_regular_file(ConfigFileName)) { - beammp_info("No config file found! Generating one..."); - CreateConfigFile(ConfigFileName); - } - if (!mFailed) { - if (fs::exists("Server.cfg")) { - beammp_warn("An old \"Server.cfg\" file still exists. Please note that this is no longer used. Instead, \"" + std::string(ConfigFileName) + "\" is used. You can safely delete the \"Server.cfg\"."); - } - ParseFromFile(ConfigFileName); - } -} - void WriteSendErrors(const std::string& name) { std::ofstream CfgFile { name, std::ios::out | std::ios::app }; CfgFile << "# You can turn on/off the SendErrors message you get on startup here" << std::endl @@ -45,8 +30,22 @@ void WriteSendErrors(const std::string& name) { << StrSendErrors << " = true" << std::endl; } +TConfig::TConfig(const std::string& ConfigFileName) + : mConfigFileName(ConfigFileName) { + if (!fs::exists(mConfigFileName) || !fs::is_regular_file(mConfigFileName)) { + beammp_info("No config file found! Generating one..."); + CreateConfigFile(mConfigFileName); + } + if (!mFailed) { + if (fs::exists("Server.cfg")) { + beammp_warn("An old \"Server.cfg\" file still exists. Please note that this is no longer used. Instead, \"" + std::string(mConfigFileName) + "\" is used. You can safely delete the \"Server.cfg\"."); + } + ParseFromFile(mConfigFileName); + } +} + void TConfig::FlushToFile() { - auto data = toml::parse(ConfigFileName); + auto data = toml::parse(mConfigFileName); data["General"] = toml::table(); data["General"][StrAuthKey.data()] = Application::Settings.Key; data["General"][StrDebug.data()] = Application::Settings.DebugModeEnabled; @@ -60,7 +59,7 @@ void TConfig::FlushToFile() { data["General"][StrResourceFolder.data()] = Application::Settings.Resource; data["General"][StrSendErrors.data()] = Application::Settings.SendErrors; data["General"][StrSendErrorsMessageEnabled.data()] = Application::Settings.SendErrorsMessageEnabled; - std::ofstream Stream(ConfigFileName); + std::ofstream Stream(mConfigFileName); Stream << data << std::flush; } @@ -104,7 +103,7 @@ void TConfig::CreateConfigFile(std::string_view name) { "# IMPORTANT: Fill in the AuthKey with the key you got from `https://beammp.com/k/dashboard` on the left under \"Keys\"\n" << '\n'; ofs << data << '\n'; - beammp_error("There was no \"" + std::string(ConfigFileName) + "\" file (this is normal for the first time running the server), so one was generated for you. It was automatically filled with the settings from your Server.cfg, if you have one. Please open ServerConfig.toml and ensure your AuthKey and other settings are filled in and correct, then restart the server. The old Server.cfg file will no longer be used and causes a warning if it exists from now on."); + beammp_error("There was no \"" + std::string(mConfigFileName) + "\" file (this is normal for the first time running the server), so one was generated for you. It was automatically filled with the settings from your Server.cfg, if you have one. Please open ServerConfig.toml and ensure your AuthKey and other settings are filled in and correct, then restart the server. The old Server.cfg file will no longer be used and causes a warning if it exists from now on."); mFailed = true; ofs.close(); // FIXME @@ -143,7 +142,7 @@ void TConfig::ParseFromFile(std::string_view name) { PrintDebug(); // all good so far, let's check if there's a key if (Application::Settings.Key.empty()) { - beammp_error("No AuthKey specified in the \"" + std::string(ConfigFileName) + "\" file. Please get an AuthKey, enter it into the config file, and restart this server."); + beammp_error("No AuthKey specified in the \"" + std::string(mConfigFileName) + "\" file. Please get an AuthKey, enter it into the config file, and restart this server."); mFailed = true; } } diff --git a/src/TServer.cpp b/src/TServer.cpp index 0ac6b0e..69d6bb9 100644 --- a/src/TServer.cpp +++ b/src/TServer.cpp @@ -15,10 +15,10 @@ namespace json = rapidjson; -TServer::TServer(int argc, char** argv) { +TServer::TServer(const std::vector& Arguments) { beammp_info("BeamMP Server v" + Application::ServerVersionString()); - if (argc > 1) { - Application::Settings.CustomIP = argv[1]; + if (Arguments.size() > 1) { + Application::Settings.CustomIP = Arguments[0]; size_t n = std::count(Application::Settings.CustomIP.begin(), Application::Settings.CustomIP.end(), '.'); auto p = Application::Settings.CustomIP.find_first_not_of(".0123456789"); if (p != std::string::npos || n != 3 || Application::Settings.CustomIP.substr(0, 3) == "127") { @@ -67,7 +67,7 @@ size_t TServer::ClientCount() const { void TServer::GlobalParser(const std::weak_ptr& Client, std::string Packet, TPPSMonitor& PPSMonitor, TNetwork& Network) { if (Packet.find("Zp") != std::string::npos && Packet.size() > 500) { - //abort(); + // abort(); } if (Packet.substr(0, 4) == "ABG:") { Packet = DeComp(Packet.substr(4)); @@ -84,7 +84,7 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac std::any Res; char Code = Packet.at(0); - //V to Z + // V to Z if (Code <= 90 && Code >= 86) { PPSMonitor.IncrementInternalPPS(); Network.SendToAll(LockedClient.get(), Packet, false, false); @@ -200,7 +200,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ int PID = -1; int VID = -1, Pos; std::string Data = Packet.substr(3), pid, vid; - switch (Code) { //Spawned Destroyed Switched/Moved NotFound Reset + switch (Code) { // Spawned Destroyed Switched/Moved NotFound Reset case 's': beammp_trace(std::string(("got 'Os' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); if (Data.at(0) == '0') { @@ -370,6 +370,6 @@ void TServer::Apply(TClient& c, int VID, const std::string& pckt) { void TServer::InsertClient(const std::shared_ptr& NewClient) { beammp_debug("inserting client (" + std::to_string(ClientCount()) + ")"); - WriteLock Lock(mClientsMutex); //TODO why is there 30+ threads locked here + WriteLock Lock(mClientsMutex); // TODO why is there 30+ threads locked here (void)mClients.insert(NewClient); } diff --git a/src/main.cpp b/src/main.cpp index e7002a1..94897bc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "TSentry.h" +#include "ArgsParser.h" #include "Common.h" #include "CustomAssert.h" #include "Http.h" @@ -17,15 +18,103 @@ #include #include +static const std::string sCommandlineArguments = R"( +USAGE: + BeamMP-Server [arguments] + +ARGUMENTS: + --help + Displays this help and exits. + --ip= + Asks the server to bind to the + specified IPv4 address. ONLY allows + IPv4 addresses of the format + A.B.C.D, where A-D are between 0 and + 255 each. + --config=/path/to/ServerConfig.toml + Absolute or relative path to the + Server Config file, including the + filename. For paths and filenames with + spaces, put quotes around the path. + --version + Prints version info and exits. + +EXAMPLES: + BeamMP-Server --ip=203.0.113.0 + Runs the BeamMP-Server and binds its address to the + specified IP '203.0.113.0'. + + BeamMP-Server --config=../MyWestCoastServerConfig.toml + Runs the BeamMP-Server and uses the server config file + which is one directory above it and is named + 'MyWestCoastServerConfig.toml'. +)"; + // this is provided by the build system, leave empty for source builds // global, yes, this is ugly, no, it cant be done another way TSentry Sentry {}; -int main(int argc, char** argv) try { +struct MainArguments { + int argc {}; + char** argv {}; + std::vector List; + std::string InvokedAs; +}; + +int BeamMPServerMain(MainArguments Arguments); + +int main(int argc, char** argv) { + MainArguments Args { argc, argv, {}, argv[0] }; + Args.List.reserve(argc); + for (int i = 1; i < argc; ++i) { + Args.List.push_back(argv[i]); + } + int MainRet = 0; + try { + MainRet = BeamMPServerMain(std::move(Args)); + } catch (const std::exception& e) { + beammp_error("A fatal exception has occurred and the server is forcefully shutting down."); + beammp_error(e.what()); + Sentry.LogException(e, _file_basename, _line); + MainRet = -1; + } + return MainRet; +} + +int BeamMPServerMain(MainArguments Arguments) { setlocale(LC_ALL, "C"); SetupSignalHandlers(); + ArgsParser Parser; + Parser.RegisterArgument({ "help" }, ArgsParser::NONE); + Parser.RegisterArgument({ "version" }, ArgsParser::NONE); + Parser.RegisterArgument({ "config" }, ArgsParser::HAS_VALUE); + Parser.RegisterArgument({ "ip" }, ArgsParser::HAS_VALUE); + Parser.Parse(Arguments.List); + if (!Parser.Verify()) { + return 1; + } + if (Parser.FoundArgument({ "help" })) { + Application::Console().Internal().set_prompt(""); + Application::Console().WriteRaw(sCommandlineArguments); + return 0; + } + if (Parser.FoundArgument({ "version" })) { + Application::Console().Internal().set_prompt(""); + Application::Console().WriteRaw("BeamMP-Server v" + Application::ServerVersionString()); + return 0; + } + + std::string ConfigPath = "ServerConfig.toml"; + if (Parser.FoundArgument({ "config" })) { + auto MaybeConfigPath = Parser.GetValueOfArgument({ "config" }); + if (MaybeConfigPath.has_value()) { + ConfigPath = MaybeConfigPath.value(); + beammp_info("Custom config requested via commandline: '" + ConfigPath + "'"); + } + } + bool Shutdown = false; Application::RegisterShutdownHandler([&Shutdown] { Shutdown = true; }); Application::RegisterShutdownHandler([] { @@ -33,8 +122,8 @@ int main(int argc, char** argv) try { TLuaEngine::WaitForAll(Futures); }); - TServer Server(argc, argv); - TConfig Config; + TServer Server(Arguments.List); + TConfig Config(ConfigPath); TLuaEngine LuaEngine; LuaEngine.SetServer(&Server); @@ -67,7 +156,4 @@ int main(int argc, char** argv) try { } beammp_info("Shutdown."); return 0; -} catch (const std::exception& e) { - beammp_error(e.what()); - Sentry.LogException(e, _file_basename, _line); }