Support new update mechanism

This commit is contained in:
momo5502 2023-02-18 19:15:47 +01:00
parent 228f943983
commit 825c9da47e
12 changed files with 825 additions and 132 deletions

View File

@ -1,150 +1,27 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "splash.hpp"
#include "updater.hpp"
#include <version.hpp>
#include "game/game.hpp"
#include <utils/io.hpp>
#include <utils/http.hpp>
#include <utils/compression.hpp>
#include <utils/progress_ui.hpp>
#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 <updater/updater.hpp>
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

View File

@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace updater
{
struct file_info
{
std::string name;
std::size_t size;
std::string hash;
};
}

View File

@ -0,0 +1,406 @@
#include <std_include.hpp>
#include "updater.hpp"
#include "updater_ui.hpp"
#include "file_updater.hpp"
#include <utils/cryptography.hpp>
#include <utils/http.hpp>
#include <utils/io.hpp>
#include <utils/compression.hpp>
#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<file_info> parse_file_infos(const std::string& json)
{
rapidjson::Document doc{};
doc.Parse(json.data(), json.size());
if (!doc.IsArray())
{
return {};
}
std::vector<file_info> 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::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
}
std::vector<file_info> 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<file_info>& 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_info> file_updater::get_outdated_files(const std::vector<file_info>& files) const
{
std::vector<file_info> 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<file_info>& 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<file_info>& outdated_files) const
{
this->listener_.update_files(outdated_files);
const auto thread_count = get_optimal_concurrent_download_count(outdated_files.size());
std::vector<std::thread> threads{};
std::atomic<size_t> current_index{0};
utils::concurrency::container<std::exception_ptr> exception{};
for (size_t i = 0; i < thread_count; ++i)
{
threads.emplace_back([&]()
{
while (!exception.access<bool>([](const std::exception_ptr& ptr)
{
return static_cast<bool>(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<file_info>& 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<file_info>& files) const
{
const auto base = std::filesystem::path(this->base_);
if (!utils::io::directory_exists(base.string()))
{
return;
}
std::vector<std::filesystem::path> 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);
}
}
}

View File

@ -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<file_info> get_outdated_files(const std::vector<file_info>& files) const;
void update_host_binary(const std::vector<file_info>& outdated_files) const;
void update_files(const std::vector<file_info>& 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<file_info>& files) const;
void cleanup_root_directory() const;
void cleanup_data_directory(const std::vector<file_info>& files) const;
};
}

View File

@ -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<file_info>& 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;
};
}

View File

@ -0,0 +1,46 @@
#include <std_include.hpp>
#include "progress_ui.hpp"
#include <utils/string.hpp>
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();
}
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <utils/com.hpp>
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<IProgressDialog> dialog_{};
};
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <stdexcept>
namespace updater
{
struct update_cancelled : public std::runtime_error
{
update_cancelled()
: std::runtime_error("Update was cancelled")
{
}
};
}

View File

@ -0,0 +1,19 @@
#include <std_include.hpp>
#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();
}
}

View File

@ -0,0 +1,8 @@
#pragma once
#include "update_cancelled.hpp"
namespace updater
{
void run(const std::filesystem::path& base);
}

View File

@ -0,0 +1,184 @@
#include <std_include.hpp>
#include "updater_ui.hpp"
#include "update_cancelled.hpp"
#include <utils/string.hpp>
namespace updater
{
updater_ui::updater_ui() = default;
updater_ui::~updater_ui() = default;
void updater_ui::update_files(const std::vector<file_info>& files)
{
this->handle_cancellation();
std::lock_guard<std::recursive_mutex> _{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<std::recursive_mutex> _{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<std::recursive_mutex> _{this->mutex_};
this->file_progress(file, 0);
this->update_file_name();
}
void updater_ui::end_file(const file_info& file)
{
std::lock_guard<std::recursive_mutex> _{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<std::recursive_mutex> _{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<std::recursive_mutex> _{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<std::recursive_mutex> _{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<std::recursive_mutex> _{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<std::recursive_mutex> _{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<std::recursive_mutex> _{this->mutex_};
return this->total_files_.size();
}
size_t updater_ui::get_downloaded_files() const
{
std::lock_guard<std::recursive_mutex> _{this->mutex_};
return this->downloaded_files_.size();
}
std::string updater_ui::get_relevant_file_name() const
{
std::lock_guard<std::recursive_mutex> _{this->mutex_};
std::string name{};
auto smallest = std::numeric_limits<size_t>::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;
}
}

View File

@ -0,0 +1,44 @@
#pragma once
#include "progress_ui.hpp"
#include "progress_listener.hpp"
#include <utils/concurrency.hpp>
namespace updater
{
class updater_ui : public progress_listener
{
public:
updater_ui();
~updater_ui();
private:
mutable std::recursive_mutex mutex_;
std::vector<file_info> total_files_{};
std::vector<file_info> downloaded_files_{};
std::unordered_map<std::string, std::pair<size_t, size_t>> downloading_files_{};
progress_ui progress_ui_{};
void update_files(const std::vector<file_info>& 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;
};
}