Add updater

This commit is contained in:
Federico Cecchetto 2022-03-04 01:39:19 +01:00
parent 5dcc59f5fa
commit e4344100fc
9 changed files with 897 additions and 24 deletions

View File

@ -0,0 +1,469 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "scheduler.hpp"
#include "dvars.hpp"
#include "updater.hpp"
#include "version.h"
#include "game/game.hpp"
#include "game/dvars.hpp"
#include <utils/nt.hpp>
#include <utils/concurrency.hpp>
#include <utils/http.hpp>
#include <utils/cryptography.hpp>
#include <utils/io.hpp>
#include <utils/string.hpp>
#define MASTER "https://master.fed0001.xyz/h1-mod/"
#define FILES_PATH "files.json"
#define FILES_PATH_DEV "files-dev.json"
#define DATA_PATH "data/"
#define DATA_PATH_DEV "data-dev/"
#define ERR_UPDATE_CHECK_FAIL "Failed to check for updates"
#define ERR_DOWNLOAD_FAIL "Failed to download file "
#define ERR_WRITE_FAIL "Failed to write file "
#define BINARY_NAME "h1-mod.exe"
namespace updater
{
namespace
{
game::dvar_t* cl_auto_update;
bool has_tried_update = false;
struct status
{
bool done;
bool success;
};
struct file_data
{
std::string name;
std::string data;
};
struct update_data_t
{
bool restart_required{};
bool cancelled{};
status check{};
status download{};
std::string error{};
std::string current_file{};
std::vector<std::string> required_files{};
};
utils::concurrency::container<update_data_t> update_data;
std::string get_branch()
{
return GIT_BRANCH;
}
std::string select(const std::string& main, const std::string& develop)
{
if (get_branch() == "develop")
{
return develop;
}
return main;
}
std::string get_data_path()
{
if (get_branch() == "develop")
{
return DATA_PATH_DEV;
}
return DATA_PATH;
}
void set_update_check_status(bool done, bool success, const std::string& error = {})
{
update_data.access([done, success, error](update_data_t& data_)
{
data_.check.done = done;
data_.check.success = success;
data_.error = error;
});
}
void set_update_download_status(bool done, bool success, const std::string& error = {})
{
update_data.access([done, success, error](update_data_t& data_)
{
data_.download.done = done;
data_.download.success = success;
data_.error = error;
});
}
bool check_file(const std::string& name, const std::string& sha)
{
std::string data;
if (!utils::io::read_file(name, &data))
{
return false;
}
if (utils::cryptography::sha1::compute(data, true) != sha)
{
return false;
}
return true;
}
std::string load_binary_name()
{
// utils::nt::library self;
// return self.get_name();
// returns the game's name and not the client's
return BINARY_NAME;
}
std::string get_binary_name()
{
static const auto name = load_binary_name();
return name;
}
std::optional<std::string> download_file(const std::string& name)
{
return utils::http::get_data(MASTER + select(DATA_PATH, DATA_PATH_DEV) + name);
}
bool is_update_cancelled()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.cancelled;
});
}
bool write_file(const std::string& name, const std::string& data)
{
if (get_binary_name() == name &&
utils::io::file_exists(name) &&
!utils::io::move_file(name, name + ".old"))
{
return false;
}
#ifdef DEBUG
return utils::io::write_file("update_test/" + name, data);
#else
return utils::io::write_file(name, data);
#endif
}
void delete_old_file()
{
utils::io::remove_file(get_binary_name() + ".old");
}
void reset_data()
{
update_data.access([](update_data_t& data_)
{
data_ = {};
});
}
std::string get_mode_flag()
{
if (game::environment::is_mp())
{
return "-multiplayer";
}
if (game::environment::is_sp())
{
return "-singleplayer";
}
return {};
}
}
// workaround
void relaunch()
{
if (!utils::io::file_exists(BINARY_NAME))
{
utils::nt::terminate(0);
return;
}
STARTUPINFOA startup_info;
PROCESS_INFORMATION process_info;
ZeroMemory(&startup_info, sizeof(startup_info));
ZeroMemory(&process_info, sizeof(process_info));
startup_info.cb = sizeof(startup_info);
char current_dir[MAX_PATH];
GetCurrentDirectoryA(sizeof(current_dir), current_dir);
char buf[1024] = {0};
const auto command_line = utils::string::va("%s %s", GetCommandLineA(), get_mode_flag().data());
strcpy_s(buf, 1024, command_line);
CreateProcess(BINARY_NAME, buf, nullptr, nullptr, false, NULL, nullptr, current_dir,
&startup_info, &process_info);
if (process_info.hThread && process_info.hThread != INVALID_HANDLE_VALUE) CloseHandle(process_info.hThread);
if (process_info.hProcess && process_info.hProcess != INVALID_HANDLE_VALUE) CloseHandle(process_info.hProcess);
utils::nt::terminate(0);
}
void set_has_tried_update(bool tried)
{
has_tried_update = tried;
}
bool get_has_tried_update()
{
return has_tried_update;
}
bool auto_updates_enabled()
{
return cl_auto_update->current.enabled;
}
bool is_update_check_done()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.check.done;
});
}
bool is_update_download_done()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.download.done;
});
}
bool get_update_check_status()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.check.success;
});
}
bool get_update_download_status()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.download.success;
});
}
bool is_update_available()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.required_files.size() > 0;
});
}
bool is_restart_required()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.restart_required;
});
}
std::string get_last_error()
{
return update_data.access<std::string>([](update_data_t& data_)
{
return data_.error;
});
}
std::string get_current_file()
{
return update_data.access<std::string>([](update_data_t& data_)
{
return data_.current_file;
});
}
void cancel_update()
{
#ifdef DEBUG
printf("[Updater] Cancelling update\n");
#endif
return update_data.access([](update_data_t& data_)
{
data_.cancelled = true;
});
}
void start_update_check()
{
cancel_update();
reset_data();
#ifdef DEBUG
printf("[Updater] starting update check\n");
#endif
scheduler::once([]()
{
const auto files_data = utils::http::get_data(MASTER + select(FILES_PATH, FILES_PATH_DEV));
if (is_update_cancelled())
{
reset_data();
return;
}
if (!files_data.has_value())
{
set_update_check_status(true, false, ERR_UPDATE_CHECK_FAIL);
return;
}
rapidjson::Document j;
j.Parse(files_data.value().data());
if (!j.IsArray())
{
set_update_check_status(true, false, ERR_UPDATE_CHECK_FAIL);
return;
}
std::vector<std::string> required_files;
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();
if (!check_file(name, sha))
{
if (get_binary_name() == name)
{
update_data.access([](update_data_t& data_)
{
data_.restart_required = true;
});
}
#ifdef DEBUG
printf("[Updater] need file %s\n", name);
#endif
required_files.push_back(name);
}
}
update_data.access([&required_files](update_data_t& data_)
{
data_.check.done = true;
data_.check.success = true;
data_.required_files = required_files;
});
}, scheduler::pipeline::async);
}
void start_update_download()
{
#ifdef DEBUG
printf("[Updater] starting update download\n");
#endif
if (!is_update_check_done() || !get_update_check_status() || is_update_cancelled())
{
return;
}
scheduler::once([]()
{
const auto required_files = update_data.access<std::vector<std::string>>([](update_data_t& data_)
{
return data_.required_files;
});
std::vector<file_data> downloads;
for (const auto& file : required_files)
{
update_data.access([file](update_data_t& data_)
{
data_.current_file = file;
});
#ifdef DEBUG
printf("[Updater] downloading file %s\n", file.data());
#endif
const auto data = download_file(file);
if (is_update_cancelled())
{
reset_data();
return;
}
if (!data.has_value())
{
set_update_download_status(true, false, ERR_DOWNLOAD_FAIL + file);
return;
}
downloads.push_back({file, data.value()});
}
for (const auto& download : downloads)
{
if (!write_file(download.name, download.data))
{
set_update_download_status(true, false, ERR_WRITE_FAIL + download.name);
return;
}
}
set_update_download_status(true, true);
}, scheduler::pipeline::async);
}
class component final : public component_interface
{
public:
void post_unpack() override
{
delete_old_file();
cl_auto_update = dvars::register_bool("cg_auto_update", true, game::DVAR_FLAG_SAVED, true);
}
};
}
REGISTER_COMPONENT(updater::component)

View File

@ -0,0 +1,26 @@
#pragma once
namespace updater
{
void relaunch();
void set_has_tried_update(bool tried);
bool get_has_tried_update();
bool auto_updates_enabled();
bool is_update_available();
bool is_update_check_done();
bool get_update_check_status();
bool is_update_download_done();
bool get_update_download_status();
bool is_restart_required();
std::string get_last_error();
std::string get_current_file();
void start_update_check();
void start_update_download();
void cancel_update();
}

View File

@ -7,6 +7,7 @@
#include "../../../component/ui_scripting.hpp"
#include "../../../component/command.hpp"
#include "../../../component/updater.hpp"
#include "component/game_console.hpp"
#include "component/scheduler.hpp"
@ -141,12 +142,35 @@ namespace ui_scripting::lua
state["LUI"] = state["luiglobals"]["LUI"];
state["Engine"] = state["luiglobals"]["Engine"];
state["Game"] = state["luiglobals"]["Game"];
auto updater_table = sol::table::create(state.lua_state());
updater_table["relaunch"] = updater::relaunch;
updater_table["sethastriedupdate"] = updater::set_has_tried_update;
updater_table["gethastriedupdate"] = updater::get_has_tried_update;
updater_table["autoupdatesenabled"] = updater::auto_updates_enabled;
updater_table["startupdatecheck"] = updater::start_update_check;
updater_table["isupdatecheckdone"] = updater::is_update_check_done;
updater_table["getupdatecheckstatus"] = updater::get_update_check_status;
updater_table["isupdateavailable"] = updater::is_update_available;
updater_table["startupdatedownload"] = updater::start_update_download;
updater_table["isupdatedownloaddone"] = updater::is_update_download_done;
updater_table["getupdatedownloadstatus"] = updater::get_update_download_status;
updater_table["cancelupdate"] = updater::cancel_update;
updater_table["isrestartrequired"] = updater::is_restart_required;
updater_table["getlasterror"] = updater::get_last_error;
updater_table["getcurrentfile"] = updater::get_current_file;
state["updater"] = updater_table;
}
}
context::context(std::string folder)
: folder_(std::move(folder))
, scheduler_(state_)
context::context(std::string data, script_type type)
: scheduler_(state_)
{
this->state_.open_libraries(sol::lib::base,
sol::lib::package,
@ -156,27 +180,37 @@ namespace ui_scripting::lua
sol::lib::math,
sol::lib::table);
this->state_["include"] = [this](const std::string& file)
{
this->load_script(file);
};
sol::function old_require = this->state_["require"];
auto base_path = utils::string::replace(this->folder_, "/", ".") + ".";
this->state_["require"] = [base_path, old_require](const std::string& path)
{
return old_require(base_path + path);
};
this->state_["scriptdir"] = [this]()
{
return this->folder_;
};
setup_types(this->state_, this->scheduler_);
printf("Loading ui script '%s'\n", this->folder_.data());
this->load_script("__init__");
if (type == script_type::file)
{
this->folder_ = data;
this->state_["include"] = [this](const std::string& file)
{
this->load_script(file);
};
sol::function old_require = this->state_["require"];
auto base_path = utils::string::replace(this->folder_, "/", ".") + ".";
this->state_["require"] = [base_path, old_require](const std::string& path)
{
return old_require(base_path + path);
};
this->state_["scriptdir"] = [this]()
{
return this->folder_;
};
printf("Loading ui script '%s'\n", this->folder_.data());
this->load_script("__init__");
}
if (type == script_type::code)
{
handle_error(this->state_.safe_script(data, &sol::script_pass_on_error));
}
}
context::~context()

View File

@ -10,10 +10,16 @@
namespace ui_scripting::lua
{
enum script_type
{
file,
code
};
class context
{
public:
context(std::string folder);
context(std::string folder, script_type type);
~context();
context(context&&) noexcept = delete;

View File

@ -11,6 +11,9 @@ namespace ui_scripting::lua::engine
{
namespace
{
const auto lui_common = utils::nt::load_resource(LUI_COMMON);
const auto lui_updater = utils::nt::load_resource(LUI_UPDATER);
auto& get_scripts()
{
static std::vector<std::unique_ptr<context>> scripts{};
@ -30,16 +33,25 @@ namespace ui_scripting::lua::engine
{
if (std::filesystem::is_directory(script) && utils::io::file_exists(script + "/__init__.lua"))
{
get_scripts().push_back(std::make_unique<context>(script));
get_scripts().push_back(std::make_unique<context>(script, script_type::file));
}
}
}
void load_code(const std::string& code)
{
get_scripts().push_back(std::make_unique<context>(code, script_type::code));
}
}
void start()
{
clear_converted_functions();
get_scripts().clear();
load_code(lui_common);
load_code(lui_updater);
load_scripts(game_module::get_host_module().get_folder() + "/data/ui_scripts/");
load_scripts("h1-mod/ui_scripts/");
load_scripts("data/ui_scripts/");

View File

@ -15,3 +15,6 @@
#define RUNNER 307
#define ICON_IMAGE 308
#define LUI_COMMON 309
#define LUI_UPDATER 310

View File

@ -118,6 +118,9 @@ RUNNER RCDATA "../../build/bin/x64/Release/runner.exe"
ICON_IMAGE RCDATA "resources/icon.png"
LUI_COMMON RCDATA "resources/ui_scripts/common.lua"
LUI_UPDATER RCDATA "resources/ui_scripts/updater.lua"
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,164 @@
menucallbacks = {}
originalmenus = {}
stack = {}
LUI.MenuBuilder.m_types_build["generic_waiting_popup_"] = function (menu, event)
local oncancel = stack.oncancel
local popup = LUI.MenuBuilder.BuildRegisteredType("waiting_popup", {
message_text = stack.text,
isLiveWithCancel = true,
cancel_func = function(...)
local args = {...}
oncancel()
LUI.FlowManager.RequestLeaveMenu(args[1])
end
})
local listchildren = popup:getChildById("LUIHorizontalList"):getchildren()
local children = listchildren[2]:getchildren()
popup.text = children[2]
stack = {
ret = popup
}
return popup
end
LUI.MenuBuilder.m_types_build["generic_yes_no_popup_"] = function()
local callback = stack.callback
local popup = LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", {
popup_title = stack.title,
message_text = stack.text,
yes_action = function()
callback(true)
end,
no_action = function()
callback(false)
end
})
stack = {
ret = popup
}
return popup
end
LUI.MenuBuilder.m_types_build["generic_confirmation_popup_"] = function()
local popup = LUI.MenuBuilder.BuildRegisteredType( "generic_confirmation_popup", {
cancel_will_close = false,
popup_title = stack.title,
message_text = stack.text,
button_text = stack.buttontext,
confirmation_action = stack.callback
})
stack = {
ret = popup
}
return stack.ret
end
LUI.onmenuopen = function(name, callback)
if (not LUI.MenuBuilder.m_types_build[name]) then
return
end
if (not menucallbacks[name]) then
menucallbacks[name] = {}
end
table.insert(menucallbacks[name], callback)
if (not originalmenus[name]) then
originalmenus[name] = LUI.MenuBuilder.m_types_build[name]
LUI.MenuBuilder.m_types_build[name] = function(...)
local args = {...}
local menu = originalmenus[name](table.unpack(args))
for k, v in luiglobals.next, menucallbacks[name] do
v(menu, table.unpack(args))
end
return menu
end
end
end
local addoptionstextinfo = LUI.Options.AddOptionTextInfo
LUI.Options.AddOptionTextInfo = function(menu)
local result = addoptionstextinfo(menu)
menu.optionTextInfo = result
return result
end
LUI.addmenubutton = function(name, data)
LUI.onmenuopen(name, function(menu)
if (not menu.list) then
return
end
local button = menu:AddButton(data.text, data.callback, nil, true, nil, {
desc_text = data.description
})
local buttonlist = menu:getChildById(menu.type .. "_list")
if (data.id) then
button.id = data.id
end
if (data.index) then
buttonlist:removeElement(button)
buttonlist:insertElement(button, data.index)
end
local hintbox = menu.optionTextInfo
menu:removeElement(hintbox)
LUI.Options.InitScrollingList(menu.list, nil)
menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu)
end)
end
LUI.openmenu = function(menu, args)
stack = args
LUI.FlowManager.RequestAddMenu(nil, menu)
return stack.ret
end
LUI.openpopupmenu = function(menu, args)
stack = args
LUI.FlowManager.RequestPopupMenu(nil, menu)
return stack.ret
end
LUI.yesnopopup = function(data)
for k, v in luiglobals.next, data do
stack[k] = v
end
LUI.FlowManager.RequestPopupMenu(nil, "generic_yes_no_popup_")
return stack.ret
end
LUI.confirmationpopup = function(data)
for k, v in luiglobals.next, data do
stack[k] = v
end
LUI.FlowManager.RequestPopupMenu(nil, "generic_confirmation_popup_")
return stack.ret
end
function userdata_:getchildren()
local children = {}
local first = self:getFirstChild()
while (first) do
table.insert(children, first)
first = first:getNextSibling()
end
return children
end

View File

@ -0,0 +1,156 @@
updatecancelled = false
taskinterval = 100
function startupdatecheck(popup, autoclose)
updatecancelled = false
local callback = function()
if (not updater.getupdatecheckstatus()) then
if (autoclose) then
LUI.FlowManager.RequestLeaveMenu(popup)
return
end
popup.text:setText("Error: " .. updater.getlasterror())
return
end
if (not updater.isupdateavailable()) then
if (autoclose) then
LUI.FlowManager.RequestLeaveMenu(popup)
return
end
popup.text:setText("No updates available")
return
end
LUI.yesnopopup({
title = "NOTICE",
text = "An update is available, proceed with installation?",
callback = function(result)
if (result) then
startupdatedownload(popup, autoclose)
else
LUI.FlowManager.RequestLeaveMenu(popup)
end
end
})
end
updater.startupdatecheck()
createtask({
done = updater.isupdatecheckdone,
cancelled = isupdatecancelled,
callback = callback,
interval = taskinterval
})
end
function startupdatedownload(popup, autoclose)
updater.startupdatedownload()
local textupdate = nil
local previousfile = nil
textupdate = game:oninterval(function()
local file = updater.getcurrentfile()
if (file == previousfile) then
return
end
file = previousfile
popup.text:setText("Downloading file " .. updater.getcurrentfile() .. "...")
end, 10)
local callback = function()
textupdate:clear()
if (not updater.getupdatedownloadstatus()) then
if (autoclose) then
LUI.FlowManager.RequestLeaveMenu(popup)
return
end
popup.text:setText("Error: " .. updater.getlasterror())
return
end
popup.text:setText("Update successful")
if (updater.isrestartrequired()) then
LUI.confirmationpopup({
title = "RESTART REQUIRED",
text = "Update requires restart",
buttontext = "RESTART",
callback = function()
updater.relaunch()
end
})--
end
if (autoclose) then
LUI.FlowManager.RequestLeaveMenu(popup)
end
end
createtask({
done = updater.isupdatedownloaddone,
cancelled = isupdatecancelled,
callback = callback,
interval = taskinterval
})
end
function updaterpopup(oncancel)
return LUI.openpopupmenu("generic_waiting_popup_", {
oncancel = oncancel,
withcancel = true,
text = "Checking for updates..."
})
end
function createtask(data)
local interval = nil
interval = game:oninterval(function()
if (data.cancelled()) then
interval:clear()
return
end
if (data.done()) then
interval:clear()
data.callback()
end
end, data.interval)
return interval
end
function isupdatecancelled()
return updatecancelled
end
function tryupdate(autoclose)
updatecancelled = false
local popup = updaterpopup(function()
updater.cancelupdate()
updatecancelled = true
end)
startupdatecheck(popup, autoclose)
end
function tryautoupdate()
if (not updater.autoupdatesenabled()) then
return
end
if (not updater.gethastriedupdate()) then
game:ontimeout(function()
updater.sethastriedupdate(true)
tryupdate(true)
end, 100)
end
end
LUI.onmenuopen("mp_main_menu", tryautoupdate)
LUI.onmenuopen("main_lockout", tryautoupdate)