From e5916f265435e998ff08cb64db97cd5c36954073 Mon Sep 17 00:00:00 2001 From: alice <58637860+fedddddd@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:02:41 +0200 Subject: [PATCH] Add updater --- src/client/component/updater.cpp | 422 +++++++++++++++++++++++++++++++ src/client/component/updater.hpp | 14 + src/common/utils/io.cpp | 12 + src/common/utils/io.hpp | 1 + src/common/utils/properties.cpp | 2 +- 5 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 src/client/component/updater.cpp create mode 100644 src/client/component/updater.hpp diff --git a/src/client/component/updater.cpp b/src/client/component/updater.cpp new file mode 100644 index 00000000..91bfee37 --- /dev/null +++ b/src/client/component/updater.cpp @@ -0,0 +1,422 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "scheduler.hpp" +#include "console/console.hpp" +#include "updater.hpp" + +#include "version.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#define FILES_PATH "files.json" +#define FILES_PATH_DEV "files-dev.json" + +#define DATA_PATH "data/" +#define DATA_PATH_DEV "data-dev/" + +namespace updater +{ + namespace + { + constexpr auto override_cache = true; + + struct file_data + { + std::string name; + std::string data; + }; + + struct file_data_previous + { + std::string name; + std::string data; + bool exists; + }; + + struct file_info + { + std::string name; + std::string hash; + }; + + std::unordered_map git_branches = + { + {"develop", branch_develop}, + {"main", branch_main}, + }; + + std::string get_branch_name(const git_branch branch) + { + for (const auto& [name, b] : git_branches) + { + if (branch == b) + { + return name; + } + } + + throw std::runtime_error("invalid branch"); + } + + git_branch load_current_branch() + { + const auto branch_str = GIT_BRANCH; + const auto iter = git_branches.find(branch_str); + if (iter != git_branches.end()) + { + return iter->second; + } + + return branch_default; + } + + git_branch get_current_branch() + { + static const auto branch = load_current_branch(); + return branch; + } + + std::string select(const std::string& main, const std::string& develop) + { + switch (updater::get_current_branch()) + { + case branch_develop: + return develop; + case branch_main: + return main; + } + + return main; + } + + std::string load_binary_name() + { + utils::nt::library self; + return self.get_name(); + } + + std::string get_binary_name() + { + static const auto name = load_binary_name(); + return name; + } + + std::optional get_server_file(const std::string& endpoint) + { + static std::vector server_urls = + { + {"https://iw7-mod.alicent.cat/"}, + {"https://iw7-mod.auroramod.dev/"}, + }; + + const auto try_url = [&](const std::string& base_url) + { + const auto url = base_url + endpoint; + console::debug("[HTTP] GET file \"%s\"\n", url.data()); + const auto result = utils::http::get_data(url); + return result; + }; + + for (const auto& url : server_urls) + { + const auto result = try_url(url); + if (result.has_value() && result->response_code == 200) + { + return result->buffer; + } + } + + return {}; + } + + bool check_file(const file_info& info) + { + std::string data; + + if (get_binary_name() == info.name) + { + if (!utils::io::read_file(info.name, &data)) + { + return false; + } + } + else + { + const auto appdata_folder = utils::properties::get_appdata_path(); + const auto path = (appdata_folder / info.name).generic_string(); + if (!utils::io::read_file(path, &data)) + { + return false; + } + } + + if (utils::cryptography::sha1::compute(data, true) != info.hash) + { + return false; + } + + return true; + } + + bool is_binary_name(const std::string& name) + { + return get_binary_name() == name; + } + + bool write_binary(const file_data& file_data) + { + return (!utils::io::file_exists(file_data.name) || + utils::io::move_file(file_data.name, file_data.name + ".old")) && + utils::io::write_file(file_data.name, file_data.data); + } + + bool write_file(const file_data& file_data) + { + const auto is_binary = is_binary_name(file_data.name); + + if (is_binary) + { + return write_binary(file_data); + } + else + { + const auto appdata_folder = utils::properties::get_appdata_path(); + const auto path = (appdata_folder / file_data.name).generic_string(); + return utils::io::write_file(path, file_data.data); + } + } + + void delete_old_file() + { + utils::io::remove_file(get_binary_name() + ".old"); + } + + std::string get_time_str() + { + return utils::string::va("%i", override_cache ? std::uint32_t(time(nullptr)) : 0); + } + + std::string format_url(const std::string& file) + { + const auto url = std::format("{}?{}", file, get_time_str()); + return url; + } + + std::optional download_file_list() + { + const auto file = format_url(select(FILES_PATH, FILES_PATH_DEV)); + return updater::get_server_file(file); + } + + std::optional download_data_file(const std::string& name) + { + const auto file = format_url(std::format("{}{}", select(DATA_PATH, DATA_PATH_DEV), name)); + return updater::get_server_file(file); + } + + std::vector get_file_list() + { + console::info("[Updater] Downloading file list\n"); + + const auto list = download_file_list(); + if (!list.has_value()) + { + console::error("[Updater] Failed to download file list\n"); + } + + rapidjson::Document j; + j.Parse(list->data()); + + std::vector parsed_list; + + const auto files = j.GetArray(); + for (const auto& file : files) + { + if (!file.IsArray() || file.Size() != 3 || !file[0].IsString() || !file[2].IsString()) + { + continue; + } + + const auto name = file[0].GetString(); + const auto sha = file[2].GetString(); + + console::info("[Updater] Add file \"%s\"\n", name); + + parsed_list.emplace_back(name, sha); + } + + return parsed_list; + } + + std::thread create_file_thread(const file_info& file, const std::function& result)>& cb) + { + return std::thread([=] + { + const auto data = download_data_file(file.name); + if (!data.has_value()) + { + console::error("[Updater] File failed to download \"%s\"\n", file.name.data()); + cb({}); + return; + } + + const auto hash = utils::cryptography::sha1::compute(data.value(), true); + if (hash != file.hash) + { + console::error("[Updater] File hash mismatch \"%s\"\n", file.name.data()); + cb({}); + return; + } + + cb(data); + }); + } + + void delete_garbage_files(const std::vector& update_files) + { + const auto appdata_folder = utils::properties::get_appdata_path(); + const auto path = (appdata_folder / CLIENT_DATA_FOLDER).generic_string(); + if (!utils::io::directory_exists(path)) + { + return; + } + + const auto current_files = utils::io::list_files_recursively(path); + for (const auto& file : current_files) + { + bool found = false; + for (const auto& update_file : update_files) + { + const auto update_file_ = (appdata_folder / update_file.name).generic_string(); + const auto path_a = std::filesystem::path(file); + const auto path_b = std::filesystem::path(update_file_); + const auto is_directory = utils::io::directory_exists(file); + const auto compare = path_a.compare(path_b); + + if ((is_directory && compare == -1) || compare == 0) + { + found = true; + break; + } + } + + if (!found) + { + console::info("[Updater] Deleting extra file %s\n", file.data()); + utils::io::remove_file(file); + } + } + } + + void run_update() + { + const auto file_list = get_file_list(); + std::vector download_threads; + + delete_garbage_files(file_list); + + utils::concurrency::container> result_data; + std::atomic_bool download_failed = false; + + for (const auto& file : file_list) + { + if (check_file(file) || download_failed) + { + continue; + } + + const auto cb = [&result_data, &download_failed, file](const std::optional& data) + { + if (!data.has_value() || download_failed) + { + download_failed = true; + return; + } + + result_data.access([=](std::vector& list) + { + list.emplace_back(file.name, data.value()); + }); + }; + + console::info("[Updater] Creating thread for file \"%s\"\n", file.name.data()); + download_threads.emplace_back(create_file_thread(file, cb)); + } + + if (download_threads.size() == 0) + { + return; + } + + for (auto& thread : download_threads) + { + if (thread.joinable()) + { + thread.join(); + } + } + + if (download_failed) + { + console::info("[Updater] Update aborted\n"); + return; + } + + std::vector previous_data; + + auto is_binary_modified = false; + + result_data.access([&](std::vector& list) + { + for (const auto& file : list) + { + if (!write_file(file)) + { + console::error("[Updater] Failed to write file \"%s\", aborting update\n", file.name.data()); + download_failed = true; + return; + } + + if (is_binary_name(file.name)) + { + is_binary_modified = true; + } + } + }); + + if (!download_failed && is_binary_modified) + { + console::info("[Updater] Restarting\n"); + utils::nt::relaunch_self(); + utils::nt::terminate(); + } + } + } + + class component final : public component_interface + { + public: + void post_start() override + { + delete_old_file(); + run_update(); + } + + void post_unpack() override + { + + } + }; +} + +REGISTER_COMPONENT(updater::component) diff --git a/src/client/component/updater.hpp b/src/client/component/updater.hpp new file mode 100644 index 00000000..3c68f170 --- /dev/null +++ b/src/client/component/updater.hpp @@ -0,0 +1,14 @@ +#pragma once + +#define CLIENT_DATA_FOLDER "cdata" + +namespace updater +{ + enum git_branch + { + branch_main = 0, + branch_develop = 1, + branch_default = branch_main, + branch_count + }; +} diff --git a/src/common/utils/io.cpp b/src/common/utils/io.cpp index 9b161d39..2e38c6e5 100644 --- a/src/common/utils/io.cpp +++ b/src/common/utils/io.cpp @@ -121,6 +121,18 @@ namespace utils::io return files; } + std::vector list_files_recursively(const std::string& directory) + { + std::vector files; + + for (auto& file : std::filesystem::recursive_directory_iterator(directory)) + { + files.push_back(file.path().generic_string()); + } + + return files; + } + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target) { std::filesystem::copy(src, target, diff --git a/src/common/utils/io.hpp b/src/common/utils/io.hpp index 38344987..19e8c143 100644 --- a/src/common/utils/io.hpp +++ b/src/common/utils/io.hpp @@ -18,5 +18,6 @@ namespace utils::io bool directory_is_empty(const std::string& directory); bool remove_directory(const std::string& directory); std::vector list_files(const std::string& directory); + std::vector list_files_recursively(const std::string& directory); void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target); } diff --git a/src/common/utils/properties.cpp b/src/common/utils/properties.cpp index 953f041c..8e5dd299 100644 --- a/src/common/utils/properties.cpp +++ b/src/common/utils/properties.cpp @@ -18,7 +18,7 @@ namespace utils::properties CoTaskMemFree(path); }); - static auto appdata = std::filesystem::path(path) / "iw7-mod"; + static auto appdata = std::filesystem::path(path) / "auroramod/iw7-mod"; return appdata; } }