diff --git a/src/client/component/updater.cpp b/src/client/component/updater.cpp index 0cab9567..63a0230a 100644 --- a/src/client/component/updater.cpp +++ b/src/client/component/updater.cpp @@ -1,150 +1,27 @@ #include #include "loader/component_loader.hpp" -#include "splash.hpp" #include "updater.hpp" - -#include +#include "game/game.hpp" #include -#include -#include -#include -#define VERSION_URL "https://nightly.link/momo5502/boiii/workflows/build/" GIT_BRANCH "/Version.zip" -#define BINARY_URL "https://nightly.link/momo5502/boiii/workflows/build/" GIT_BRANCH "/Release%20Binary.zip" +#include namespace updater { - namespace - { - std::string get_version_zip() - { - const auto version_zip = utils::http::get_data(VERSION_URL); - if (!version_zip || version_zip->empty()) - { - throw std::runtime_error("Invalid version data"); - } - - return *version_zip; - } - - std::string get_version() - { - const auto zip = get_version_zip(); - auto res = utils::compression::zip::extract(zip); - return res["version.txt"]; - } - - bool requires_update() - { - return get_version() != GIT_HASH; - } - - std::string get_self_file() - { - const auto self = utils::nt::library::get_by_address(get_self_file); - return self.get_path().generic_string(); - } - - std::string get_leftover_file() - { - return get_self_file() + ".old"; - } - - std::string download_update() - { - const auto data = utils::http::get_data(BINARY_URL); - - if (!data) - { - throw std::runtime_error("Invalid binary"); - } - - return *data; - } - - void activate_update() - { - utils::nt::relaunch_self(); - TerminateProcess(GetCurrentProcess(), 0); - } - - std::string get_binary(const std::string& data) - { - auto res = utils::compression::zip::extract(data); - if (res.size() == 1) - { - return std::move(res.begin()->second); - } - - throw std::runtime_error("Invalid data"); - } - - void cleanup_update() - { - const auto leftover_file = get_leftover_file(); - for (size_t i = 0; i < 3; ++i) - { - if (utils::io::remove_file(leftover_file)) - { - break; - } - - std::this_thread::sleep_for(1s); - } - } - - void perform_update(const HWND parent_window) - { - const utils::progress_ui progress_ui{}; - progress_ui.set_title("Updating BOIII"); - progress_ui.set_line(1, "Downloading update..."); - progress_ui.show(true, parent_window); - - const auto update_data = download_update(); - - if (progress_ui.is_cancelled()) - { - return; - } - - // Is it good to add artificial sleeps? - // Makes the ui nice, for sure. - std::this_thread::sleep_for(2s); - - progress_ui.set_line(1, "Installing update..."); - progress_ui.set_progress(1, 1); - - const auto self_file = get_self_file(); - const auto leftover_file = get_leftover_file(); - - const auto binary = get_binary(update_data); - - cleanup_update(); - utils::io::move_file(self_file, leftover_file); - utils::io::write_file(self_file, binary); - - std::this_thread::sleep_for(2s); - } - } - void update() { - cleanup_update(); - -#if defined(NDEBUG) && defined(CI) try { - if (requires_update()) - { - perform_update(splash::get_window()); - activate_update(); - } + run(game::get_appdata_path()); + } + catch (update_cancelled&) + { + TerminateProcess(GetCurrentProcess(), 0); } catch (...) { } -#endif } class component final : public generic_component @@ -186,6 +63,4 @@ namespace updater }; } -#if defined(NDEBUG) && defined(CI) REGISTER_COMPONENT(updater::component) -#endif diff --git a/src/client/updater/file_info.hpp b/src/client/updater/file_info.hpp new file mode 100644 index 00000000..74f1ee38 --- /dev/null +++ b/src/client/updater/file_info.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace updater +{ + struct file_info + { + std::string name; + std::size_t size; + std::string hash; + }; +} diff --git a/src/client/updater/file_updater.cpp b/src/client/updater/file_updater.cpp new file mode 100644 index 00000000..f20d53d5 --- /dev/null +++ b/src/client/updater/file_updater.cpp @@ -0,0 +1,406 @@ +#include + +#include "updater.hpp" +#include "updater_ui.hpp" +#include "file_updater.hpp" + +#include +#include +#include +#include + +#define UPDATE_SERVER "https://updater.xlabs.dev/" + +#define UPDATE_FILE_MAIN UPDATE_SERVER "boiii.json" +#define UPDATE_FOLDER_MAIN UPDATE_SERVER "boiii/" + +#define UPDATE_HOST_BINARY "boiii.exe" + +namespace updater +{ + namespace + { + std::string get_update_file() + { + return UPDATE_FILE_MAIN; + } + + std::string get_update_folder() + { + return UPDATE_FOLDER_MAIN; + } + + std::vector parse_file_infos(const std::string& json) + { + rapidjson::Document doc{}; + doc.Parse(json.data(), json.size()); + + if (!doc.IsArray()) + { + return {}; + } + + std::vector files{}; + + for (const auto& element : doc.GetArray()) + { + if (!element.IsArray()) + { + continue; + } + + auto array = element.GetArray(); + + file_info info{}; + info.name.assign(array[0].GetString(), array[0].GetStringLength()); + info.size = array[1].GetInt64(); + info.hash.assign(array[2].GetString(), array[2].GetStringLength()); + + files.emplace_back(std::move(info)); + } + + return files; + } + + std::string get_cache_buster() + { + return "?" + std::to_string( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + } + + std::vector get_file_infos() + { + const auto json = utils::http::get_data(get_update_file() + get_cache_buster()); + if (!json) + { + return {}; + } + + return parse_file_infos(*json); + } + + std::string get_hash(const std::string& data) + { + return utils::cryptography::sha1::compute(data, true); + } + + const file_info* find_host_file_info(const std::vector& outdated_files) + { + for (const auto& file : outdated_files) + { + if (file.name == UPDATE_HOST_BINARY) + { + return &file; + } + } + + return nullptr; + } + + size_t get_optimal_concurrent_download_count(const size_t file_count) + { + size_t cores = std::thread::hardware_concurrency(); + cores = (cores * 2) / 3; + return std::max(1ull, std::min(cores, file_count)); + } + + bool is_inside_folder(const std::filesystem::path& file, const std::filesystem::path& folder) + { + const auto relative = std::filesystem::relative(file, folder); + const auto start = relative.begin(); + return start != relative.end() && start->string() != ".."; + } + } + + file_updater::file_updater(progress_listener& listener, std::filesystem::path base, + std::filesystem::path process_file) + : listener_(listener) + , base_(std::move(base)) + , process_file_(std::move(process_file)) + , dead_process_file_(process_file_) + { + this->dead_process_file_.replace_extension(".exe.old"); + this->delete_old_process_file(); + } + + void file_updater::run() const + { + const auto files = get_file_infos(); + if (!files.empty()) + { + this->cleanup_directories(files); + } + + const auto outdated_files = this->get_outdated_files(files); + if (outdated_files.empty()) + { + return; + } + + this->update_host_binary(outdated_files); + this->update_files(outdated_files); + + std::this_thread::sleep_for(1s); + } + + void file_updater::update_file(const file_info& file) const + { + const auto url = get_update_folder() + file.name + "?" + file.hash; + + const auto data = utils::http::get_data(url, {}, [&](const size_t progress) + { + this->listener_.file_progress(file, progress); + }); + + if (!data || (data->size() != file.size || get_hash(*data) != file.hash)) + { + throw std::runtime_error("Failed to download: " + url); + } + + const auto out_file = this->get_drive_filename(file); + if (!utils::io::write_file(out_file, *data, false)) + { + throw std::runtime_error("Failed to write: " + file.name); + } + } + + std::vector file_updater::get_outdated_files(const std::vector& files) const + { + std::vector outdated_files{}; + + for (const auto& info : files) + { + if (this->is_outdated_file(info)) + { + outdated_files.emplace_back(info); + } + } + + return outdated_files; + } + + void file_updater::update_host_binary(const std::vector& outdated_files) const + { + const auto* host_file = find_host_file_info(outdated_files); + if (!host_file) + { + return; + } + + try + { + this->move_current_process_file(); + this->update_files({*host_file}); + } + catch (...) + { + this->restore_current_process_file(); + throw; + } + + utils::nt::relaunch_self(); + throw update_cancelled(); + } + + void file_updater::update_files(const std::vector& outdated_files) const + { + this->listener_.update_files(outdated_files); + + const auto thread_count = get_optimal_concurrent_download_count(outdated_files.size()); + + std::vector threads{}; + std::atomic current_index{0}; + + utils::concurrency::container exception{}; + + for (size_t i = 0; i < thread_count; ++i) + { + threads.emplace_back([&]() + { + while (!exception.access([](const std::exception_ptr& ptr) + { + return static_cast(ptr); + })) + { + const auto index = current_index++; + if (index >= outdated_files.size()) + { + break; + } + + try + { + const auto& file = outdated_files[index]; + this->listener_.begin_file(file); + this->update_file(file); + this->listener_.end_file(file); + } + catch (...) + { + exception.access([](std::exception_ptr& ptr) + { + ptr = std::current_exception(); + }); + + return; + } + } + }); + } + + for (auto& thread : threads) + { + if (thread.joinable()) + { + thread.join(); + } + } + + exception.access([](const std::exception_ptr& ptr) + { + if (ptr) + { + std::rethrow_exception(ptr); + } + }); + + this->listener_.done_update(); + } + + bool file_updater::is_outdated_file(const file_info& file) const + { +#if !defined(NDEBUG) || !defined(CI) + if (file.name == UPDATE_HOST_BINARY) + { + return false; + } +#endif + + std::string data{}; + const auto drive_name = this->get_drive_filename(file); + if (!utils::io::read_file(drive_name, &data)) + { + return true; + } + + if (data.size() != file.size) + { + return true; + } + + const auto hash = get_hash(data); + return hash != file.hash; + } + + std::filesystem::path file_updater::get_drive_filename(const file_info& file) const + { + if (file.name == UPDATE_HOST_BINARY) + { + return this->process_file_; + } + + return this->base_ / file.name; + } + + void file_updater::move_current_process_file() const + { + utils::io::move_file(this->process_file_, this->dead_process_file_); + } + + void file_updater::restore_current_process_file() const + { + utils::io::move_file(this->dead_process_file_, this->process_file_); + } + + void file_updater::delete_old_process_file() const + { + // Wait for other process to die + for (auto i = 0; i < 4; ++i) + { + utils::io::remove_file(this->dead_process_file_); + if (!utils::io::file_exists(this->dead_process_file_)) + { + break; + } + + std::this_thread::sleep_for(2s); + } + } + + void file_updater::cleanup_directories(const std::vector& files) const + { + if (!utils::io::directory_exists(this->base_)) + { + return; + } + + this->cleanup_root_directory(); + this->cleanup_data_directory(files); + } + + void file_updater::cleanup_root_directory() const + { + const auto existing_files = utils::io::list_files(this->base_); + for (const auto& file : existing_files) + { + const auto entry = std::filesystem::relative(file, this->base_); + if ((entry.string() == "user" || entry.string() == "data") && utils::io::directory_exists(file)) + { + continue; + } + + std::error_code code{}; + std::filesystem::remove_all(file, code); + } + } + + void file_updater::cleanup_data_directory(const std::vector& files) const + { + const auto base = std::filesystem::path(this->base_); + if (!utils::io::directory_exists(base.string())) + { + return; + } + + std::vector legal_files{}; + legal_files.reserve(files.size()); + for (const auto& file : files) + { + if (file.name != UPDATE_HOST_BINARY) + { + legal_files.emplace_back(std::filesystem::absolute(base / file.name)); + } + } + + const auto existing_files = utils::io::list_files(base.string(), true); + for (auto& file : existing_files) + { + const auto is_file = std::filesystem::is_regular_file(file); + const auto is_folder = std::filesystem::is_directory(file); + + if (is_file || is_folder) + { + bool is_legal = false; + + for (const auto& legal_file : legal_files) + { + if ((is_folder && is_inside_folder(legal_file, file)) || + (is_file && legal_file == file)) + { + is_legal = true; + break; + } + } + + if (is_legal) + { + continue; + } + } + + std::error_code code{}; + std::filesystem::remove_all(file, code); + } + } +} diff --git a/src/client/updater/file_updater.hpp b/src/client/updater/file_updater.hpp new file mode 100644 index 00000000..626ef134 --- /dev/null +++ b/src/client/updater/file_updater.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "progress_listener.hpp" + +namespace updater +{ + class file_updater + { + public: + file_updater(progress_listener& listener, std::filesystem::path base, std::filesystem::path process_file); + + void run() const; + + [[nodiscard]] std::vector get_outdated_files(const std::vector& files) const; + + void update_host_binary(const std::vector& outdated_files) const; + + void update_files(const std::vector& outdated_files) const; + + private: + progress_listener& listener_; + + std::filesystem::path base_; + std::filesystem::path process_file_; + std::filesystem::path dead_process_file_; + + void update_file(const file_info& file) const; + + [[nodiscard]] bool is_outdated_file(const file_info& file) const; + [[nodiscard]] std::filesystem::path get_drive_filename(const file_info& file) const; + + void move_current_process_file() const; + void restore_current_process_file() const; + void delete_old_process_file() const; + + void cleanup_directories(const std::vector& files) const; + void cleanup_root_directory() const; + void cleanup_data_directory(const std::vector& files) const; + }; +} diff --git a/src/client/updater/progress_listener.hpp b/src/client/updater/progress_listener.hpp new file mode 100644 index 00000000..74d43003 --- /dev/null +++ b/src/client/updater/progress_listener.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "file_info.hpp" + +namespace updater +{ + class progress_listener + { + public: + virtual ~progress_listener() = default; + + virtual void update_files(const std::vector& files) = 0; + virtual void done_update() = 0; + + virtual void begin_file(const file_info& file) = 0; + virtual void end_file(const file_info& file) = 0; + + virtual void file_progress(const file_info& file, size_t progress) = 0; + }; +} diff --git a/src/client/updater/progress_ui.cpp b/src/client/updater/progress_ui.cpp new file mode 100644 index 00000000..9ded7335 --- /dev/null +++ b/src/client/updater/progress_ui.cpp @@ -0,0 +1,46 @@ +#include +#include "progress_ui.hpp" + +#include + +namespace updater +{ + progress_ui::progress_ui() + { + this->dialog_ = utils::com::create_progress_dialog(); + if (!this->dialog_) + { + throw std::runtime_error{"Failed to create dialog"}; + } + } + + progress_ui::~progress_ui() + { + this->dialog_->StopProgressDialog(); + } + + void progress_ui::show() const + { + this->dialog_->StartProgressDialog(nullptr, nullptr, PROGDLG_AUTOTIME, nullptr); + } + + void progress_ui::set_progress(const size_t current, const size_t max) const + { + this->dialog_->SetProgress64(current, max); + } + + void progress_ui::set_line(const int line, const std::string& text) const + { + this->dialog_->SetLine(line, utils::string::convert(text).data(), false, nullptr); + } + + void progress_ui::set_title(const std::string& title) const + { + this->dialog_->SetTitle(utils::string::convert(title).data()); + } + + bool progress_ui::is_cancelled() const + { + return this->dialog_->HasUserCancelled(); + } +} diff --git a/src/client/updater/progress_ui.hpp b/src/client/updater/progress_ui.hpp new file mode 100644 index 00000000..d8cf273e --- /dev/null +++ b/src/client/updater/progress_ui.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace updater +{ + class progress_ui + { + public: + progress_ui(); + ~progress_ui(); + + void show() const; + + void set_progress(size_t current, size_t max) const; + void set_line(int line, const std::string& text) const; + void set_title(const std::string& title) const; + + bool is_cancelled() const; + + private: + CComPtr dialog_{}; + }; +} diff --git a/src/client/updater/update_cancelled.hpp b/src/client/updater/update_cancelled.hpp new file mode 100644 index 00000000..e6b074da --- /dev/null +++ b/src/client/updater/update_cancelled.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace updater +{ + struct update_cancelled : public std::runtime_error + { + update_cancelled() + : std::runtime_error("Update was cancelled") + { + } + }; +} diff --git a/src/client/updater/updater.cpp b/src/client/updater/updater.cpp new file mode 100644 index 00000000..22a68da1 --- /dev/null +++ b/src/client/updater/updater.cpp @@ -0,0 +1,19 @@ +#include + +#include "updater.hpp" +#include "updater_ui.hpp" +#include "file_updater.hpp" + +namespace updater +{ + void run(const std::filesystem::path& base) + { + const utils::nt::library self; + const auto self_file = self.get_path(); + + updater_ui updater_ui{}; + const file_updater file_updater{updater_ui, base, self_file}; + + file_updater.run(); + } +} diff --git a/src/client/updater/updater.hpp b/src/client/updater/updater.hpp new file mode 100644 index 00000000..6eb83b1a --- /dev/null +++ b/src/client/updater/updater.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "update_cancelled.hpp" + +namespace updater +{ + void run(const std::filesystem::path& base); +} diff --git a/src/client/updater/updater_ui.cpp b/src/client/updater/updater_ui.cpp new file mode 100644 index 00000000..34070f09 --- /dev/null +++ b/src/client/updater/updater_ui.cpp @@ -0,0 +1,184 @@ +#include +#include "updater_ui.hpp" +#include "update_cancelled.hpp" + +#include + +namespace updater +{ + updater_ui::updater_ui() = default; + updater_ui::~updater_ui() = default; + + void updater_ui::update_files(const std::vector& files) + { + this->handle_cancellation(); + + std::lock_guard _{this->mutex_}; + this->total_files_ = files; + this->downloaded_files_.clear(); + this->downloading_files_.clear(); + + this->progress_ui_ = {}; + this->progress_ui_.set_title("BOIII Updater"); + this->progress_ui_.show(); + + // Is it good to add artificial sleeps? + // Makes the ui nice, for sure. + std::this_thread::sleep_for(1s); + } + + void updater_ui::done_update() + { + std::lock_guard _{this->mutex_}; + + this->progress_ui_.set_progress(1, 1); + this->update_file_name(); + + this->total_files_.clear(); + this->downloaded_files_.clear(); + this->downloading_files_.clear(); + + std::this_thread::sleep_for(2s); + } + + void updater_ui::begin_file(const file_info& file) + { + this->handle_cancellation(); + + std::lock_guard _{this->mutex_}; + + this->file_progress(file, 0); + this->update_file_name(); + } + + void updater_ui::end_file(const file_info& file) + { + std::lock_guard _{this->mutex_}; + + this->downloaded_files_.emplace_back(file); + const auto entry = this->downloading_files_.find(file.name); + if (entry != this->downloading_files_.end()) + { + this->downloading_files_.erase(entry); + } + else + { + assert(false && "Failed to erase file."); + } + + this->update_progress(); + this->update_file_name(); + } + + void updater_ui::file_progress(const file_info& file, const size_t progress) + { + this->handle_cancellation(); + + std::lock_guard _{this->mutex_}; + + this->downloading_files_[file.name] = {progress, file.size}; + this->update_progress(); + } + + void updater_ui::handle_cancellation() const + { + if (this->progress_ui_.is_cancelled()) + { + throw update_cancelled(); + } + } + + void updater_ui::update_progress() const + { + std::lock_guard _{this->mutex_}; + this->progress_ui_.set_progress(this->get_downloaded_size(), this->get_total_size()); + } + + void updater_ui::update_file_name() const + { + std::lock_guard _{this->mutex_}; + + const auto downloaded_file_count = this->get_downloaded_files(); + const auto total_file_count = this->get_total_files(); + + if (downloaded_file_count == total_file_count) + { + this->progress_ui_.set_line(1, "Update successful."); + } + else + { + this->progress_ui_.set_line(1, utils::string::va("Updating files... (%zu/%zu)", downloaded_file_count, + total_file_count)); + } + + this->progress_ui_.set_line(2, this->get_relevant_file_name()); + } + + size_t updater_ui::get_total_size() const + { + std::lock_guard _{this->mutex_}; + + size_t total_size = 0; + for (const auto& file : this->total_files_) + { + total_size += file.size; + } + + return total_size; + } + + size_t updater_ui::get_downloaded_size() const + { + std::lock_guard _{this->mutex_}; + + size_t downloaded_size = 0; + for (const auto& file : this->downloaded_files_) + { + downloaded_size += file.size; + } + + for (const auto& file : this->downloading_files_) + { + downloaded_size += file.second.first; + } + + return downloaded_size; + } + + size_t updater_ui::get_total_files() const + { + std::lock_guard _{this->mutex_}; + return this->total_files_.size(); + } + + size_t updater_ui::get_downloaded_files() const + { + std::lock_guard _{this->mutex_}; + return this->downloaded_files_.size(); + } + + std::string updater_ui::get_relevant_file_name() const + { + std::lock_guard _{this->mutex_}; + + std::string name{}; + auto smallest = std::numeric_limits::max(); + + for (const auto& file : this->downloading_files_) + { + const auto max_size = file.second.second; + if (max_size < smallest) + { + smallest = max_size; + name = file.first; + } + } + + if (name.empty() && !this->downloaded_files_.empty()) + { + name = this->downloaded_files_.back().name; + } + + return name; + } +} diff --git a/src/client/updater/updater_ui.hpp b/src/client/updater/updater_ui.hpp new file mode 100644 index 00000000..7f653e09 --- /dev/null +++ b/src/client/updater/updater_ui.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "progress_ui.hpp" +#include "progress_listener.hpp" + +#include + +namespace updater +{ + class updater_ui : public progress_listener + { + public: + updater_ui(); + ~updater_ui(); + + private: + mutable std::recursive_mutex mutex_; + std::vector total_files_{}; + std::vector downloaded_files_{}; + std::unordered_map> downloading_files_{}; + + progress_ui progress_ui_{}; + + void update_files(const std::vector& files) override; + void done_update() override; + + void begin_file(const file_info& file) override; + void end_file(const file_info& file) override; + + void file_progress(const file_info& file, size_t progress) override; + + void handle_cancellation() const; + void update_progress() const; + void update_file_name() const; + + size_t get_total_size() const; + size_t get_downloaded_size() const; + + size_t get_total_files() const; + size_t get_downloaded_files() const; + + std::string get_relevant_file_name() const; + }; +}