diff --git a/CMakeLists.txt b/CMakeLists.txt index a87bb37..0ab9aca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ set(PRJ_HEADERS include/TScopedTimer.h include/TServer.h include/VehicleData.h + include/Update.h ) # add all source files (.cpp) to this, except the one with main() set(PRJ_SOURCES @@ -70,6 +71,7 @@ set(PRJ_SOURCES src/TScopedTimer.cpp src/TServer.cpp src/VehicleData.cpp + src/Update.cpp ) find_package(Lua REQUIRED) diff --git a/include/Update.h b/include/Update.h new file mode 100644 index 0000000..2dfabee --- /dev/null +++ b/include/Update.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace Update { + +[[noreturn]] void PerformUpdate(const std::string& InvokedAs); + +} diff --git a/src/Update.cpp b/src/Update.cpp new file mode 100644 index 0000000..9342ad6 --- /dev/null +++ b/src/Update.cpp @@ -0,0 +1,248 @@ +#include "Update.h" +#include "Common.h" +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux) +#include +#elif defined(WIN32) +#include +#endif + +static bool ProgressReport(uint64_t current, uint64_t total) { + if (current == total) { + fmt::print("100% ({:.2f} / {:.2f} KiB)\n", float(current) / 1024.0f, float(total) / 1024.0f); + } + if (total != 0) { + auto percent = (float(current) / float(total)) * 100.0f; + fmt::print("{:3.0f}% ({:.2f} / {:.2f} KiB)\r", percent, float(current) / 1024.0f, float(total) / 1024.0f); + } + return true; +} + +static std::unordered_map sDistroMap = { + { "debian:11", "-debian" }, + { "ubuntu:22.04", "-ubuntu" }, +}; + +void Update::PerformUpdate(const std::string& InvokedAs) { + using json = nlohmann::json; + namespace http = httplib; + + constexpr auto GH = "api.github.com"; + std::string ReleasesAPI = "/repos/BeamMP/BeamMP-Server/releases"; + + http::Headers APIHeaders {}; + APIHeaders.emplace("X-GitHub-Api-Version", "2022-11-28"); + APIHeaders.emplace("Accept", "application/vnd.github+json"); + + http::SSLClient c(GH); + c.set_read_timeout(std::chrono::seconds(30)); + + beammp_infof("Checking for latest release..."); + // check if there is a new release + auto Res = c.Get(ReleasesAPI + "/latest", APIHeaders); + if (!Res || Res->status < 200 || Res->status >= 300) { + beammp_errorf("Failed to fetch latest release: {}", to_string(Res.error())); + std::exit(1); + } + + Version NewVersion { 0, 0, 0 }; + + json ReleaseInfo; + + try { + ReleaseInfo = json::parse(Res->body); + + std::string TagName = ReleaseInfo["tag_name"].get(); + if (!TagName.starts_with("v") || std::count(TagName.begin(), TagName.end(), '.') != 2) { + beammp_errorf("Invalid version provided by GitHub: '{}', exiting", TagName); + std::exit(1); + } + TagName = TagName.substr(1); + auto Tag = Application::VersionStrToInts(TagName); + NewVersion = Version { Tag[0], Tag[1], Tag[2] }; + beammp_infof("Latest release is v{}", NewVersion.AsString()); + } catch (const std::exception& e) { + beammp_errorf("Failed to fetch latest release info from GitHub"); + beammp_errorf("Error: {}", e.what()); + std::exit(1); + } + + // check if the new release is patch, minor or major + auto Current = Application::ServerVersion(); + + bool MajorOutdated = NewVersion.major > Current.major; + bool MinorOutdated = !MajorOutdated && Application::IsOutdated(Current, NewVersion); + + if (!MajorOutdated && !MinorOutdated) { + beammp_infof("BeamMP-Server is already the latest version (v{})!", Current.AsString()); + std::exit(0); + } else { + beammp_infof("New update available, updating from v{} to v{}", Current.AsString(), NewVersion.AsString()); + } + + // see https://github.com/cpredef/predef for information +#if !defined(__amd64__) \ + && !defined(__amd64) \ + && !defined(__x86_64__) \ + && !defined(__x86_64) \ + && !defined(_M_X64) \ + && !defined(_M_AMD64) + beammp_errorf("BeamMP doesn't provide binaries for your CPU architecture (only x86_64). Please update manually"); +#endif + + std::string Postfix = {}; +// figure out current platform +#if WIN32 + beammp_infof("Current platform is Windows"); + Postfix = ".exe"; +#elif __linux + beammp_infof("Current platform is Linux, checking distribution"); + const std::string OsReleasePath = "/etc/os-release"; + + std::string DistroID = ""; + std::string DistroVersion = ""; + + if (fs::exists(OsReleasePath)) { + std::ifstream OsRelease(OsReleasePath); + std::string Line {}; + while (std::getline(OsRelease, Line)) { + if (Line.starts_with("ID=")) { + DistroID = Line.substr(3); + } else if (Line.starts_with("VERSION_ID=")) { + DistroVersion = Line.substr(strlen("VERSION_ID=")); + } else if (Line.starts_with("VERSION_ID=\"")) { + DistroVersion = Line.substr(strlen("VERSION_ID=")); + // skip closing quote + DistroVersion = DistroVersion.substr(0, DistroVersion.size() - 1); + } + } + } + beammp_infof("Distribution: {} {}", DistroID, DistroVersion); + const auto Distro = DistroID + ":" + DistroVersion; + + if (sDistroMap.contains(Distro)) { + Postfix = sDistroMap[Distro]; + } else { + beammp_errorf("BeamMP doesn't provide binaries for this distribution, please update manually"); + std::exit(1); + } +#else + beammp_infof("BeamMP doesn't provide binaries for this platform, please update manually"); + std::exit(1); +#endif + + // check if the release exists for that platform + std::string DownloadURL = ""; + try { + for (const auto& Asset : ReleaseInfo.at("assets")) { + if (Asset.at("name").get() == "BeamMP-Server" + Postfix) { + DownloadURL = Asset.at("browser_download_url"); + break; + } + } + } catch (const std::exception& e) { + beammp_errorf("Failed to parse GitHub API's release assets: {}", e.what()); + std::exit(1); + } + if (DownloadURL.empty()) { + beammp_infof("BeamMP doesn't provide binaries for this platform or distribution (postfix '{}' not found in the release assets), please update manually", Postfix); + std::exit(1); + } + + // download urls exist, ask if the user wants to do a major update + if (MajorOutdated) { + beammp_warnf("The update from v{} to v{} is a major update, which is likely to *break* any Lua Plugins. Please make sure you have read the release notes at {} before proceeding!", Current.AsString(), NewVersion.AsString(), ReleaseInfo.at("html_url").get()); +#if !defined(WIN32) + if (!isatty(STDIN_FILENO)) { + beammp_errorf("Refusing to do a major version update non-interactively. Run this again in a TTY or update manually"); + std::exit(1); + } +#endif + fmt::print("\n"); + int ch; + do { + fmt::print("Do you wish to proceed with this update? [y/n] "); + std::string Input; + std::getline(std::cin, Input); + if (Input.empty()) { + continue; + } + ch = Input.at(0); + } while (tolower(ch) != 'y' && tolower(ch) != 'n'); + if (tolower(ch) == 'n') { + beammp_error("Cancelling update"); + std::exit(2); + } + } + + beammp_info("Downloading latest release from github.com..."); + + http::SSLClient DlClient("github.com"); + DlClient.set_follow_location(true); + auto ReleaseRes = DlClient.Get(DownloadURL.substr(strlen("https://github.com")), ProgressReport); + + if (!ReleaseRes || ReleaseRes->status < 200 || ReleaseRes->status >= 300) { + beammp_errorf("Failed to fetch binary: {}. Please update manually", to_string(ReleaseRes.error())); + std::exit(1); + } + + beammp_info("Download complete!"); + + auto Temp = InvokedAs + ".temp"; + beammp_infof("Creating '{}'", Temp); + FILE* Out = std::fopen(Temp.c_str(), "w+"); + if (!Out) { + beammp_errorf("Failed to update executable, because a temporary file couldn't be created: {}", std::strerror(errno)); + std::exit(1); + } + auto n = std::fwrite(ReleaseRes->body.data(), 1, ReleaseRes->body.size(), Out); + if (n != ReleaseRes->body.size()) { + beammp_errorf("Failed to update executable, because a temporary file couldn't be written to: {}", std::strerror(errno)); + std::fclose(Out); + std::exit(1); + } + std::fclose(Out); + +#if defined(__linux) + beammp_infof("Removing '{}'", InvokedAs); + struct stat st; + if (stat(InvokedAs.c_str(), &st) != 0) { + // shouldn't happen at this point + beammp_errorf("Failed to stat original executable: {}", std::strerror(errno)); + std::exit(1); + } + auto Ret = unlink(InvokedAs.c_str()); + if (Ret != 0) { + beammp_errorf("Failed to remove executable: {}", std::strerror(errno)); + std::exit(1); + } + beammp_infof("Replacing '{}' with '{}'", InvokedAs, Temp); + fs::rename(Temp, InvokedAs); + if (chmod(InvokedAs.c_str(), st.st_mode) != 0) { + beammp_warnf("Failed to set file mode to 0{:o}: {}. File may not be executable.", st.st_mode, std::strerror(errno)); + } +#elif defined(WIN32) + auto DeleteMe = InvokedAs + ".delete_me"; + std::filesystem::rename(InvokedAs, DeleteMe); + std::filesystem::rename(Temp, InvokedAs); + std::wstring Wide(DeleteMe.begin(), DeleteMe.end()); + int Attr = GetFileAttributes(Wide.c_str()); + if ((Attr & FILE_ATTRIBUTE_HIDDEN) == 0) { + SetFileAttributes(Wide.c_str(), Attr | FILE_ATTRIBUTE_HIDDEN); + } +#else + beammp_error("Not implemented"); + std::exit(4); +#endif + + // make sure the user knows that it was a success, on windows wait for return??? + + std::exit(0); +} diff --git a/src/main.cpp b/src/main.cpp index d73c6c1..b13d16d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ #include "TPluginMonitor.h" #include "TResourceManager.h" #include "TServer.h" +#include "Update.h" #include #include @@ -33,6 +34,9 @@ ARGUMENTS: including the path given in --config. --version Prints version info and exits. + --update + Starts an interactive update to the newest + version of BeamMP-Server. EXAMPLES: BeamMP-Server --config=../MyWestCoastServerConfig.toml @@ -72,6 +76,7 @@ int BeamMPServerMain(MainArguments Arguments) { ArgsParser Parser; Parser.RegisterArgument({ "help" }, ArgsParser::NONE); Parser.RegisterArgument({ "version" }, ArgsParser::NONE); + Parser.RegisterArgument({ "update" }, ArgsParser::NONE); Parser.RegisterArgument({ "config" }, ArgsParser::HAS_VALUE); Parser.RegisterArgument({ "working-directory" }, ArgsParser::HAS_VALUE); Parser.Parse(Arguments.List); @@ -86,6 +91,10 @@ int BeamMPServerMain(MainArguments Arguments) { Application::Console().WriteRaw("BeamMP-Server v" + Application::ServerVersionString()); return 0; } + if (Parser.FoundArgument({ "update" })) { + Update::PerformUpdate(Arguments.InvokedAs); + return 123; + } std::string ConfigPath = "ServerConfig.toml"; if (Parser.FoundArgument({ "config" })) {