From c42c748b37a96b7b628dd30e82d8f554d6f134d3 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 20 Jan 2022 15:46:13 +0100 Subject: [PATCH 1/9] start fixing backend heartbeat --- CMakeLists.txt | 2 ++ include/Common.h | 11 ++++++++--- include/Http.h | 2 +- src/Common.cpp | 2 +- src/Http.cpp | 5 ++++- src/THeartbeatThread.cpp | 34 +++++++++++++--------------------- 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a8f37ca..4f818d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,8 @@ project(BeamMP-Server HOMEPAGE_URL https://beammp.com LANGUAGES CXX C) +set(HTTPLIB_REQUIRE_OPENSSL ON) + include_directories("${PROJECT_SOURCE_DIR}/deps/asio/asio/include") include_directories("${PROJECT_SOURCE_DIR}/deps/rapidjson/include") include_directories("${PROJECT_SOURCE_DIR}/deps/websocketpp") diff --git a/include/Common.h b/include/Common.h index c61f98b..4b8a1e0 100644 --- a/include/Common.h +++ b/include/Common.h @@ -74,10 +74,15 @@ public: static TSettings Settings; + static std::vector GetBackendUrlsInOrder() { + return { + "backend.beammp.com", + "backup1.beammp.com", + "backup2.beammp.com" + }; + } + static std::string GetBackendUrlForAuth() { return "auth.beammp.com"; } - static std::string GetBackendHostname() { return "backend.beammp.com"; } - static std::string GetBackup1Hostname() { return "backup1.beammp.com"; } - static std::string GetBackup2Hostname() { return "backup2.beammp.com"; } static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; } static void CheckForUpdates(); static std::array VersionStrToInts(const std::string& str); diff --git a/include/Http.h b/include/Http.h index 62ded67..3ff2cd5 100644 --- a/include/Http.h +++ b/include/Http.h @@ -25,7 +25,7 @@ constexpr size_t RSA_DEFAULT_KEYLENGTH { 2048 }; namespace Http { std::string GET(const std::string& host, int port, const std::string& target, unsigned int* status = nullptr); -std::string POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr); +std::string POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr, const httplib::Headers& headers = {}); namespace Status { std::string ToString(int code); } diff --git a/src/Common.cpp b/src/Common.cpp index 4ed5814..a6ef230 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -99,7 +99,7 @@ void Application::CheckForUpdates() { Application::SetSubsystemStatus("UpdateCheck", Application::Status::Starting); // checks current version against latest version std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" }; - auto Response = Http::GET(GetBackendHostname(), 443, "/v/s"); + auto Response = Http::GET(GetBackendUrlsInOrder().at(0), 443, "/v/s"); bool Matches = std::regex_match(Response, VersionRegex); if (Matches) { auto MyVersion = ServerVersion(); diff --git a/src/Http.cpp b/src/Http.cpp index 11bea18..cc91e04 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -4,6 +4,7 @@ #include "Common.h" #include "CustomAssert.h" #include "LuaAPI.h" +#include "httplib.h" #include #include @@ -33,8 +34,9 @@ std::string Http::GET(const std::string& host, int port, const std::string& targ } } -std::string Http::POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status) { +std::string Http::POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status, const httplib::Headers& headers) { httplib::SSLClient client(host, port); + beammp_assert(client.is_valid()); client.enable_server_certificate_verification(false); client.set_address_family(AF_INET); auto res = client.Post(target.c_str(), body.c_str(), body.size(), ContentType.c_str()); @@ -44,6 +46,7 @@ std::string Http::POST(const std::string& host, int port, const std::string& tar } return res->body; } else { + beammp_debug("POST failed: " + httplib::to_string(res.error())); return Http::ErrorString; } } diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index 27ef253..4311cf5 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -54,34 +54,26 @@ void THeartbeatThread::operator()() { auto Target = "/heartbeat"; unsigned int ResponseCode = 0; - T = Http::POST(Application::GetBackendHostname(), 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode); - if ((T.substr(0, 2) != "20" && ResponseCode != 200) || ResponseCode != 200) { - beammp_trace("got " + T + " from backend"); - Application::SetSubsystemStatus("Heartbeat", Application::Status::Bad); - SentryReportError(Application::GetBackendHostname() + Target, ResponseCode); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - T = Http::POST(Application::GetBackup1Hostname(), 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode); + bool Ok = true; + json::Document Doc; + for (const auto& Hostname : Application::GetBackendUrlsInOrder()) { + T = Http::POST(Hostname, 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode, { { "api-v", "2" } }); if ((T.substr(0, 2) != "20" && ResponseCode != 200) || ResponseCode != 200) { - SentryReportError(Application::GetBackup1Hostname() + Target, ResponseCode); + beammp_trace("heartbeat to " + Hostname + " returned: " + T); Application::SetSubsystemStatus("Heartbeat", Application::Status::Bad); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - T = Http::POST(Application::GetBackup2Hostname(), 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode); - if ((T.substr(0, 2) != "20" && ResponseCode != 200) || ResponseCode != 200) { - beammp_warn("Backend system refused server! Server will not show in the public server list."); - Application::SetSubsystemStatus("Heartbeat", Application::Status::Bad); - isAuth = false; - SentryReportError(Application::GetBackup2Hostname() + Target, ResponseCode); - } else { - Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); - } + isAuth = false; + SentryReportError(Hostname + Target, ResponseCode); + Ok = false; } else { Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); + Ok = true; + break; } - } else { - Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); } - + if (!Ok) { + beammp_warn("Backend system refused server! Server will not show in the public server list."); + } if (!isAuth) { if (T == "2000") { beammp_info(("Authenticated!")); From cd4129e05d4f614e8f75200c478d9f6f1b054926 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 20 Jan 2022 16:09:08 +0100 Subject: [PATCH 2/9] add api-v header to heartbeat post --- src/Http.cpp | 3 +- src/THeartbeatThread.cpp | 68 +++++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/Http.cpp b/src/Http.cpp index cc91e04..467fd3c 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -36,10 +36,11 @@ std::string Http::GET(const std::string& host, int port, const std::string& targ std::string Http::POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status, const httplib::Headers& headers) { httplib::SSLClient client(host, port); + client.set_read_timeout(std::chrono::seconds(10)); beammp_assert(client.is_valid()); client.enable_server_certificate_verification(false); client.set_address_family(AF_INET); - auto res = client.Post(target.c_str(), body.c_str(), body.size(), ContentType.c_str()); + auto res = client.Post(target.c_str(), headers, body.c_str(), body.size(), ContentType.c_str()); if (res) { if (status) { *status = res->status; diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index 4311cf5..f5be47b 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -55,32 +55,70 @@ void THeartbeatThread::operator()() { auto Target = "/heartbeat"; unsigned int ResponseCode = 0; - bool Ok = true; json::Document Doc; - for (const auto& Hostname : Application::GetBackendUrlsInOrder()) { - T = Http::POST(Hostname, 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode, { { "api-v", "2" } }); - if ((T.substr(0, 2) != "20" && ResponseCode != 200) || ResponseCode != 200) { - beammp_trace("heartbeat to " + Hostname + " returned: " + T); - Application::SetSubsystemStatus("Heartbeat", Application::Status::Bad); - isAuth = false; - SentryReportError(Hostname + Target, ResponseCode); - Ok = false; + bool Ok = false; + for (const auto& Url : Application::GetBackendUrlsInOrder()) { + T = Http::POST(Url, 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode, { { "api-v", "2" } }); + beammp_trace(T); + Doc.Parse(T.data(), T.size()); + if (Doc.HasParseError() || !Doc.IsObject()) { + beammp_error("Backend response failed to parse as valid json"); + beammp_debug("Response was: `" + T + "`"); + Sentry.SetContext("JSON Response", { { "reponse", T } }); + SentryReportError(Url + Target, ResponseCode); + } else if (ResponseCode != 200) { + SentryReportError(Url + Target, ResponseCode); } else { - Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); + // all ok Ok = true; break; } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); } - if (!Ok) { - beammp_warn("Backend system refused server! Server will not show in the public server list."); + std::string Status {}; + std::string Code {}; + std::string Message {}; + const auto StatusKey = "status"; + const auto CodeKey = "code"; + const auto MessageKey = "msg"; + + if (Ok) { + if (Doc.HasMember(StatusKey) && Doc[StatusKey].IsString()) { + Status = Doc[StatusKey].GetString(); + } else { + Sentry.SetContext("JSON Response", { { StatusKey, "invalid string / missing" } }); + Ok = false; + } + if (Doc.HasMember(CodeKey) && Doc[CodeKey].IsString()) { + Code = Doc[CodeKey].GetString(); + } else { + Sentry.SetContext("JSON Response", { { CodeKey, "invalid string / missing" } }); + Ok = false; + } + if (Doc.HasMember(MessageKey) && Doc[MessageKey].IsString()) { + Message = Doc[MessageKey].GetString(); + } else { + Sentry.SetContext("JSON Response", { { MessageKey, "invalid string / missing" } }); + Ok = false; + } + if (!Ok) { + beammp_error("Missing/invalid json members in backend response"); + Sentry.LogError("Missing/invalid json members in backend response", __FILE__, std::to_string(__LINE__)); + } } - if (!isAuth) { - if (T == "2000") { + + if (Ok && !isAuth) { + if (Status == "2000") { beammp_info(("Authenticated!")); isAuth = true; - } else if (T == "200") { + } else if (Status == "200") { beammp_info(("Resumed authenticated session!")); isAuth = true; + } else { + if (Message.empty()) { + Message = "Backend didn't provide a reason"; + } + beammp_error("Backend REFUSED the auth key. " + Message); } } } From 2a588954be19a6b6c61000649d3af10a802837a6 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 20 Jan 2022 21:31:00 +0100 Subject: [PATCH 3/9] advance to 3.0.1 --- include/Common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/Common.h b/include/Common.h index 4b8a1e0..2cf15b9 100644 --- a/include/Common.h +++ b/include/Common.h @@ -119,7 +119,7 @@ private: static inline std::mutex mShutdownHandlersMutex {}; static inline std::deque mShutdownHandlers {}; - static inline Version mVersion { 3, 0, 0 }; + static inline Version mVersion { 3, 0, 1 }; }; std::string ThreadName(bool DebugModeOverride = false); From fca5bbcec9cdfd687fc9ac2c6744eb6879fdb010 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Fri, 21 Jan 2022 14:26:42 +0100 Subject: [PATCH 4/9] UpdateCheck: Try all URLs --- src/Common.cpp | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Common.cpp b/src/Common.cpp index a6ef230..a113f74 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -99,25 +99,31 @@ void Application::CheckForUpdates() { Application::SetSubsystemStatus("UpdateCheck", Application::Status::Starting); // checks current version against latest version std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" }; - auto Response = Http::GET(GetBackendUrlsInOrder().at(0), 443, "/v/s"); - bool Matches = std::regex_match(Response, VersionRegex); - if (Matches) { - auto MyVersion = ServerVersion(); - auto RemoteVersion = Version(VersionStrToInts(Response)); - if (IsOutdated(MyVersion, RemoteVersion)) { - std::string RealVersionString = RemoteVersion.AsString(); - beammp_warn(std::string(ANSI_YELLOW_BOLD) + "NEW VERSION OUT! There's a new version (v" + RealVersionString + ") of the BeamMP-Server available! For more info visit https://wiki.beammp.com/en/home/server-maintenance#updating-the-server." + std::string(ANSI_RESET)); + for (const auto& url : GetBackendUrlsInOrder()) { + auto Response = Http::GET(GetBackendUrlsInOrder().at(0), 443, "/v/s"); + bool Matches = std::regex_match(Response, VersionRegex); + if (Matches) { + auto MyVersion = ServerVersion(); + auto RemoteVersion = Version(VersionStrToInts(Response)); + if (IsOutdated(MyVersion, RemoteVersion)) { + std::string RealVersionString = RemoteVersion.AsString(); + beammp_warn(std::string(ANSI_YELLOW_BOLD) + "NEW VERSION OUT! There's a new version (v" + RealVersionString + ") of the BeamMP-Server available! For more info visit https://wiki.beammp.com/en/home/server-maintenance#updating-the-server." + std::string(ANSI_RESET)); + } else { + beammp_info("Server up-to-date!"); + } + Application::SetSubsystemStatus("UpdateCheck", Application::Status::Good); + break; } else { - beammp_info("Server up-to-date!"); + beammp_debug("Failed to fetch version from: " + url); + beammp_trace("got " + Response); + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("get-response", { { "response", Response } }); + Sentry.LogError("failed to get server version", _file_basename, _line); + Application::SetSubsystemStatus("UpdateCheck", Application::Status::Bad); } - Application::SetSubsystemStatus("UpdateCheck", Application::Status::Good); - } else { - beammp_warn("Unable to fetch version from backend."); - beammp_trace("got " + Response); - auto Lock = Sentry.CreateExclusiveContext(); - Sentry.SetContext("get-response", { { "response", Response } }); - Sentry.LogError("failed to get server version", _file_basename, _line); - Application::SetSubsystemStatus("UpdateCheck", Application::Status::Bad); + } + if (Application::GetSubsystemStatuses().at("UpdateCheck") == Application::Status::Bad) { + beammp_warn("Unable to fetch version info from backend."); } } From e043361abb0a70c7b992b66021a5c311f54e9346 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 3 Feb 2022 18:30:00 +0100 Subject: [PATCH 5/9] update libraries --- deps/commandline | 2 +- deps/cpp-httplib | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/commandline b/deps/commandline index 3d11606..0d3e107 160000 --- a/deps/commandline +++ b/deps/commandline @@ -1 +1 @@ -Subproject commit 3d11606d02b449b8afd40a7132160d2392043eb3 +Subproject commit 0d3e1073c1005270dfad851c1f8c59f4ce29d8c1 diff --git a/deps/cpp-httplib b/deps/cpp-httplib index 301faa0..b324921 160000 --- a/deps/cpp-httplib +++ b/deps/cpp-httplib @@ -1 +1 @@ -Subproject commit 301faa074c4a0fa1dbe470dfb4f77912caa1c57f +Subproject commit b324921c1aeff2976544128e4bb2a0979a4aa595 From 29f8d29e3307dd00123289d80697fb5bd6c26114 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 3 Feb 2022 18:30:42 +0100 Subject: [PATCH 6/9] Move commandline initialization after cwd setting This fixes an issue where the log file is written to the original directory, even if --working-directory=path was used. This can obviously be pretty bad. --- src/main.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 59764f2..a1c9354 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,11 +77,6 @@ int main(int argc, char** argv) { int BeamMPServerMain(MainArguments Arguments) { setlocale(LC_ALL, "C"); - Application::InitializeConsole(); - Application::SetSubsystemStatus("Main", Application::Status::Starting); - - SetupSignalHandlers(); - ArgsParser Parser; Parser.RegisterArgument({ "help" }, ArgsParser::NONE); Parser.RegisterArgument({ "version" }, ArgsParser::NONE); @@ -121,6 +116,11 @@ int BeamMPServerMain(MainArguments Arguments) { } } } + + Application::InitializeConsole(); + Application::SetSubsystemStatus("Main", Application::Status::Starting); + + SetupSignalHandlers(); bool Shutdown = false; Application::RegisterShutdownHandler([&Shutdown] { From 754053e73fc39573d17a82e17b59ca25f23f171e Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 3 Feb 2022 18:57:52 +0100 Subject: [PATCH 7/9] Use yield() where possible Replaced calls of this_thread::sleep_* with this_thread::yield(), which yields the thread to the OS' scheduler. --- include/TLuaEngine.h | 1 + src/TConsole.cpp | 2 +- src/TLuaEngine.cpp | 34 ++++++++++++++-------------------- src/TPPSMonitor.cpp | 4 ++-- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/include/TLuaEngine.h b/include/TLuaEngine.h index 1673c61..9b0a884 100644 --- a/include/TLuaEngine.h +++ b/include/TLuaEngine.h @@ -149,6 +149,7 @@ public: void CancelEventTimers(const std::string& EventName, TLuaStateId StateId); sol::state_view GetStateForPlugin(const fs::path& PluginPath); TLuaStateId GetStateIDForPlugin(const fs::path& PluginPath); + void AddResultToCheck(const std::shared_ptr& Result); static constexpr const char* BeamMPFnNotFoundError = "BEAMMP_FN_NOT_FOUND"; diff --git a/src/TConsole.cpp b/src/TConsole.cpp index 79df8d2..827881c 100644 --- a/src/TConsole.cpp +++ b/src/TConsole.cpp @@ -386,7 +386,7 @@ TConsole::TConsole() { } else { auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(cmd), "", "" }); while (!Future->Ready) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); // TODO: Add a timeout + std::this_thread::yield(); // TODO: Add a timeout } if (Future->Error) { beammp_lua_error(Future->ErrorMessage); diff --git a/src/TLuaEngine.cpp b/src/TLuaEngine.cpp index 89742e2..41ffb96 100644 --- a/src/TLuaEngine.cpp +++ b/src/TLuaEngine.cpp @@ -54,25 +54,17 @@ void TLuaEngine::operator()() { auto ResultCheckThread = std::thread([&] { RegisterThread("ResultCheckThread"); while (!mShutdown) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::unique_lock Lock(mResultsToCheckMutex); if (!mResultsToCheck.empty()) { auto Res = mResultsToCheck.front(); mResultsToCheck.pop(); Lock.unlock(); - size_t Waited = 0; - while (!Res->Ready) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - Waited++; - if (Waited > 250) { - // FIXME: This should *eventually* timeout. - // beammp_lua_error(Res->Function + " in " + Res->StateId + " took >1s to respond, not printing possible errors"); - Lock.lock(); - mResultsToCheck.push(Res); - Lock.unlock(); - break; - } + if (!Res->Ready) { + Lock.lock(); + mResultsToCheck.push(Res); + Lock.unlock(); } if (Res->Error) { if (Res->ErrorMessage != BeamMPFnNotFoundError) { @@ -80,13 +72,14 @@ void TLuaEngine::operator()() { } } } + std::this_thread::yield(); } }); // event loop auto Before = std::chrono::high_resolution_clock::now(); while (!mShutdown) { if (mLuaStates.size() == 0) { - std::this_thread::sleep_for(std::chrono::seconds(500)); + std::this_thread::sleep_for(std::chrono::seconds(100)); } { // Timed Events Scope std::unique_lock Lock(mTimedEventsMutex); @@ -149,6 +142,11 @@ TLuaStateId TLuaEngine::GetStateIDForPlugin(const fs::path& PluginPath) { return ""; } +void TLuaEngine::AddResultToCheck(const std::shared_ptr& Result) { + std::unique_lock Lock(mResultsToCheckMutex); + mResultsToCheck.push(Result); +} + void TLuaEngine::WaitForAll(std::vector>& Results, const std::optional& Max) { for (const auto& Result : Results) { bool Cancelled = false; @@ -705,6 +703,7 @@ void TLuaEngine::StateThreadData::AddPath(const fs::path& Path) { void TLuaResult::WaitUntilReady() { while (!Ready) { + std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } @@ -762,12 +761,7 @@ void TPluginMonitor::operator()() { auto StateID = mEngine.GetStateIDForPlugin(fs::path(Pair.first).parent_path()); auto Res = mEngine.EnqueueScript(StateID, Chunk); // TODO: call onInit - while (!Res->Ready) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - if (Res->Error) { - beammp_lua_error(Res->ErrorMessage); - } + mEngine.AddResultToCheck(Res); } else { // TODO: trigger onFileChanged event beammp_trace("Change detected in file \"" + Pair.first + "\", event trigger not implemented yet"); diff --git a/src/TPPSMonitor.cpp b/src/TPPSMonitor.cpp index 8d58d84..b5b4951 100644 --- a/src/TPPSMonitor.cpp +++ b/src/TPPSMonitor.cpp @@ -21,8 +21,8 @@ TPPSMonitor::TPPSMonitor(TServer& Server) void TPPSMonitor::operator()() { RegisterThread("PPSMonitor"); while (!mNetwork) { - // hard spi - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // hard(-ish) spin + std::this_thread::yield(); } beammp_debug("PPSMonitor starting"); Application::SetSubsystemStatus("PPSMonitor", Application::Status::Good); From 5c44a307bcae9eb57977ea1030faa5d1e401916a Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 3 Feb 2022 19:03:21 +0100 Subject: [PATCH 8/9] update changelog --- Changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Changelog.md b/Changelog.md index 6774147..b633919 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,9 @@ +# v3.0.1 + +- ADDED Backup URLs to UpdateCheck (will fail less often now) +- FIXED a bug where, when run with --working-directory, the Server.log would still be in the original directory +- FIXED a bug which could cause the plugin reload thread to spin at 100% if the reloaded plugin's onInit didn't terminate + # v3.0.0 - CHANGED entire plugin Lua implementation (rewrite) From 9494bc70fb25f3d3305e521cbd8a11f0a3ea6039 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 3 Feb 2022 19:46:31 +0100 Subject: [PATCH 9/9] fix changelog --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index b633919..7c54dd3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,7 @@ - ADDED Backup URLs to UpdateCheck (will fail less often now) - FIXED a bug where, when run with --working-directory, the Server.log would still be in the original directory -- FIXED a bug which could cause the plugin reload thread to spin at 100% if the reloaded plugin's onInit didn't terminate +- FIXED a bug which could cause the plugin reload thread to spin at 100% if the reloaded plugin's didn't terminate # v3.0.0