Stand/Stand/PackageMgr.cpp
2024-10-16 11:20:42 +08:00

460 lines
9.1 KiB
C++

#include "PackageMgr.hpp"
#include <soup/os.hpp>
#include <fmt/format.h>
#include <soup/FileReader.hpp>
#include <soup/FileWriter.hpp>
#include "Auth.hpp"
#include "Codename.hpp"
#include "Exceptional.hpp"
#include "ExecCtx.hpp"
#include "get_appdata_path.hpp"
#include "Gui.hpp"
#include "HttpRequestBuilder.hpp"
#include "Label.hpp"
#include "lang.hpp"
#include "NetInterface.hpp"
#include "PackageInstallTask.hpp"
#include "str2int.hpp"
#include "StringUtils.hpp"
#include "ThreadContext.hpp"
#include "Util.hpp"
namespace Stand
{
bool Package::operator==(const Package& b) const noexcept
{
return name == b.name;
}
bool Package::operator!=(const Package& b) const noexcept
{
return !operator==(b);
}
bool Package::isDependency() const noexcept
{
return type != STANDALONE;
}
bool Package::isInstalled() const noexcept
{
return !local.files.empty();
}
bool Package::existsOnRemote() const noexcept
{
return !remote.files.empty();
}
bool Package::isUpToDate() const noexcept
{
return remote.id == local.id;
}
bool Package::shouldShowToUser() const noexcept
{
return !hidden || isInstalled();
}
bool Package::dependsOn(const std::string& dep_name) const noexcept
{
return std::find(remote.dependencies.cbegin(), remote.dependencies.cend(), dep_name) != remote.dependencies.cend();
}
bool Package::isDependedOnByInstalledPackage() const noexcept
{
for (const auto& package : PackageMgr::repo.packages)
{
if (package.isInstalled() && package.dependsOn(this->name))
{
return true;
}
}
return false;
}
std::string Package::getType() const
{
return name.substr(0, name.find('/'));
}
std::string Package::getName() const
{
return name.substr(name.find('/') + 1);
}
Label Package::getMenuName() const
{
auto name = getName();
if (!existsOnRemote())
{
name.insert(0, "[NOT ON REMOTE] ");
}
return LIT(std::move(name));
}
bool Package::isLua() const noexcept
{
return name.substr(0, 4) == "lua/";
}
bool Package::isLuaCompatLayer() const noexcept
{
return name.substr(0, 17) == "lua_compat_layer/";
}
std::filesystem::path Package::getFilePath(const std::string& rel) const
{
std::wstring path = get_appdata_path();
if (isLua() || isLuaCompatLayer())
{
path.append(L"Lua Scripts\\");
}
else
{
SOUP_ASSERT_UNREACHABLE;
}
path.append(StringUtils::utf8_to_utf16(rel));
return path;
}
bool Package::doAllLocalFilesExist() const
{
for (const auto& file : local.files)
{
if (!std::filesystem::exists(getFilePath(file)))
{
return false;
}
}
return true;
}
void Package::setBusyForInstall()
{
status = INSTALLING;
}
void Package::install()
{
setBusyForInstall();
for (const auto& dep : remote.dependencies)
{
auto pkg = PackageMgr::find(dep);
if (!pkg->isInstalled())
{
pkg->install();
}
}
SOUP_ASSERT(existsOnRemote(), "Can't install local-only package");
local.id = remote.id;
local.files.reserve(remote.files.size());
auto task = g_net_interface.add<PackageInstallTask>(*this);
while (!task->isWorkDone())
{
ExecCtx::get().yield();
}
status = NONE;
SOUP_ASSERT(task->fi == remote.files.size(), "Install task finished abnormally");
SOUP_ASSERT(isInstalled(), "Package is not installed after install task finished");
if (!PackageMgr::packages_reported_as_installed.contains(this->name))
{
PackageMgr::packages_reported_as_installed.emplace(this->name);
g_auth.reportEvent("PI", this->name);
}
}
void Package::uninstall(bool keep_trash)
{
deleteFiles(local.files);
local.id.clear();
local.files.clear();
if (!keep_trash)
{
deleteFiles(remote.trash);
}
}
void Package::deleteFiles(const std::vector<std::string>& files) const
{
std::error_code ec{};
for (const auto& file : files)
{
bool used_by_another_package = false;
for (auto& package : PackageMgr::repo.packages)
{
if (package.isInstalled() && *this != package)
{
for (const auto& file2 : package.local.files)
{
if (file == file2)
{
used_by_another_package = true;
break;
}
}
}
if (used_by_another_package)
{
break;
}
}
if (used_by_another_package)
{
continue;
}
auto path = getFilePath(file);
if (std::filesystem::is_regular_file(path))
{
std::filesystem::remove(path, ec);
if (file.find('/') != std::string::npos)
{
deleteEmptyUpwards(path.parent_path());
}
}
else if (std::filesystem::is_directory(path))
{
deleteEmptyUpwards(path);
}
}
}
void Package::deleteEmptyUpwards(std::filesystem::path path)
{
std::error_code ec{};
while (std::filesystem::is_empty(path, ec))
{
std::filesystem::remove(path, ec);
path = path.parent_path();
}
}
bool Package::isBusy() const noexcept
{
return status != NONE;
}
void Package::userInstall()
{
install();
if (isDependency())
{
PackageMgr::deps_manually_installed.emplace_back(name);
}
PackageMgr::saveInstalledPackages();
}
std::wstring PackageMgr::getInstalledPackagesFile()
{
return get_appdata_path("Installed Packages.bin");
}
void PackageMgr::tunablesData(const soup::JsonObject& obj)
{
loadInstalledPackages();
repo.fromJson(obj);
repo.sortPackages();
Exceptional::createManagedExceptionalThread(__FUNCTION__, []
{
// If we were to update sooner, some scripts might be missing for populate & load state.
while (!g_gui.worker_thread_running)
{
if (g_gui.isUnloadPending())
{
return;
}
soup::os::sleep(50);
}
bool any_updates = false;
for (auto& pkg : repo.packages)
{
if (!pkg.isInstalled())
{
continue;
}
packages_reported_as_installed.emplace(pkg.name);
if (pkg.existsOnRemote())
{
bool should_update = false;
if (!pkg.isUpToDate())
{
should_update = true;
#ifndef STAND_DEBUG
if (g_gui.meta_state.data.contains(soup::ObfusString("Packages Source")))
#endif
{
Util::toast(fmt::format(fmt::runtime(soup::ObfusString("Updating package {} from {} to {}").str()), pkg.name, pkg.local.id, pkg.remote.id));
}
}
else if (!pkg.doAllLocalFilesExist())
{
should_update = true;
Util::toast(LANG_FMT("REPO_MISNGLCL", pkg.name));
}
if (should_update)
{
pkg.status = Package::UPDATING;
pkg.uninstall(true);
pkg.install();
any_updates = true;
}
}
}
if (any_updates)
{
autoRemoveDependencies();
saveInstalledPackages();
}
updating_completed.fulfil();
});
}
void PackageMgr::tunablesFail()
{
loadInstalledPackages();
repo.sortPackages();
updating_completed.fulfil();
}
void PackageMgr::loadInstalledPackages()
{
soup::FileReader in(getInstalledPackagesFile());
if (in.hasMore()
&& in.vec_str_nt_u64_dyn(deps_manually_installed)
)
{
while (in.hasMore())
{
Package pkg{};
uint64_t num_files;
if (!in.str_nt(pkg.name)
|| !in.str_nt(pkg.local.id)
|| !in.u64_dyn(num_files)
)
{
break;
}
while (num_files-- > 0)
{
std::string fn;
if (!in.str_nt(fn))
{
break;
}
if (fn.empty())
{
continue;
}
if (fn.substr(0, 4) != "EXT:")
{
pkg.local.files.emplace_back(std::move(fn));
continue;
}
if (fn.substr(4, 4) == "TYP:")
{
pkg.type = static_cast<Package::Type>(str2int<uint8_t>(fn.substr(8)).value());
}
else if (fn.substr(4, 4) == "FLG:")
{
auto flags = str2int<uint8_t>(fn.substr(8)).value();
if ((flags >> 1) & 1)
{
pkg.type = Package::LIBRARY;
}
}
}
if (pkg.type == Package::STANDALONE
&& pkg.local.id.empty()
)
{
pkg.type = Package::LIBRARY;
}
repo.packages.emplace_back(std::move(pkg));
}
}
}
Package* PackageMgr::find(const std::string& name)
{
return repo.find(name);
}
Package* PackageMgr::findByFile(const std::filesystem::path& abs_path)
{
return repo.findByFile(abs_path);
}
void PackageMgr::autoRemoveDependencies()
{
bool uninstalled_any = false;
for (auto& package : repo.packages)
{
if (package.isInstalled()
&& package.isDependency()
&& std::find(deps_manually_installed.cbegin(), deps_manually_installed.cend(), package.name) == deps_manually_installed.cend()
&& !package.isDependedOnByInstalledPackage()
)
{
package.uninstall();
uninstalled_any = true;
}
}
if (uninstalled_any)
{
autoRemoveDependencies();
}
}
void PackageMgr::saveInstalledPackages()
{
soup::FileWriter of(getInstalledPackagesFile());
for (auto i = deps_manually_installed.begin(); i != deps_manually_installed.end(); )
{
if (find(*i) == nullptr)
{
deps_manually_installed.erase(i);
}
else
{
++i;
}
}
if (of.vec_str_nt_u64_dyn(deps_manually_installed))
{
for (const auto& package : repo.packages)
{
if (package.isInstalled())
{
auto data = package.local.files;
data.emplace_back(std::move(std::string("EXT:TYP:").append(fmt::to_string(package.type))));
if (!of.str_nt(package.name)
|| !of.str_nt(package.local.id)
|| !of.vec_str_nt_u64_dyn(data)
)
{
break;
}
}
}
}
}
}