Support new update mechanism
This commit is contained in:
parent
228f943983
commit
825c9da47e
@ -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
|
||||
|
13
src/client/updater/file_info.hpp
Normal file
13
src/client/updater/file_info.hpp
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace updater
|
||||
{
|
||||
struct file_info
|
||||
{
|
||||
std::string name;
|
||||
std::size_t size;
|
||||
std::string hash;
|
||||
};
|
||||
}
|
406
src/client/updater/file_updater.cpp
Normal file
406
src/client/updater/file_updater.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
40
src/client/updater/file_updater.hpp
Normal file
40
src/client/updater/file_updater.hpp
Normal 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;
|
||||
};
|
||||
}
|
20
src/client/updater/progress_listener.hpp
Normal file
20
src/client/updater/progress_listener.hpp
Normal 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;
|
||||
};
|
||||
}
|
46
src/client/updater/progress_ui.cpp
Normal file
46
src/client/updater/progress_ui.cpp
Normal 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();
|
||||
}
|
||||
}
|
24
src/client/updater/progress_ui.hpp
Normal file
24
src/client/updater/progress_ui.hpp
Normal 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_{};
|
||||
};
|
||||
}
|
14
src/client/updater/update_cancelled.hpp
Normal file
14
src/client/updater/update_cancelled.hpp
Normal 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")
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
19
src/client/updater/updater.cpp
Normal file
19
src/client/updater/updater.cpp
Normal 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();
|
||||
}
|
||||
}
|
8
src/client/updater/updater.hpp
Normal file
8
src/client/updater/updater.hpp
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "update_cancelled.hpp"
|
||||
|
||||
namespace updater
|
||||
{
|
||||
void run(const std::filesystem::path& base);
|
||||
}
|
184
src/client/updater/updater_ui.cpp
Normal file
184
src/client/updater/updater_ui.cpp
Normal 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;
|
||||
}
|
||||
}
|
44
src/client/updater/updater_ui.hpp
Normal file
44
src/client/updater/updater_ui.hpp
Normal 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;
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user