feat(updater): final step for portable install

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers 2023-06-24 22:21:16 -07:00
parent d2a3acd493
commit 4313466589
No known key found for this signature in database
GPG Key ID: E10E321EB160949B
8 changed files with 256 additions and 55 deletions

View File

@ -148,8 +148,8 @@ set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_M
# Build platform.
set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
# Channel list URL
set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.")
# Github repo URL with releases for updater
set(Launcher_UPDATER_GITHUB_REPO "" CACHE STRING "Base URL for the updater.")
# The metadata server
set(Launcher_META_URL "https://meta.prismlauncher.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.")

View File

@ -108,10 +108,11 @@ ModDetails ReadMCModTOML(QByteArray contents)
return {};
}
#else
tomlData = toml::parse(contents.toStdString());
if (!tomlData) {
toml::parse_result result = toml::parse(contents.toStdString());
if (!result) {
return {};
}
tomlData = result.table();
#endif
// array defined by [[mods]]

View File

@ -241,12 +241,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
return {};
}
#else
table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
if (!table) {
toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
if (!result) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(table.error().what());
qWarning() << "Reason: " << result.error().description();
return {};
}
table = result.table();
#endif
// index_file.close();

View File

@ -68,6 +68,8 @@ ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Pr
setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true);
setSkipButton(false);
changeProgress(0, 100);
updateSize();
adjustSize();
}
void ProgressDialog::setSkipButton(bool present, QString label)
@ -96,7 +98,11 @@ ProgressDialog::~ProgressDialog()
void ProgressDialog::updateSize()
{
QSize lastSize = this->size();
QSize qSize = QSize(480, minimumSizeHint().height());
auto min_height = minimumSizeHint().height();
if (ui->taskProgressScrollArea->isHidden())
min_height -= ui->taskProgressScrollArea->minimumSizeHint().height();
min_height = std::max(min_height, 0);
QSize qSize = QSize(480, min_height);
// if the current window is too small
if ((lastSize != qSize) && (lastSize.height() < qSize.height()))
@ -111,7 +117,6 @@ void ProgressDialog::updateSize()
}
setMinimumSize(qSize);
}
int ProgressDialog::execWithTask(Task* task)

View File

@ -48,6 +48,9 @@
<property name="text">
<string>Global Task Status...</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
@ -109,8 +112,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>464</width>
<height>96</height>
<width>460</width>
<height>108</height>
</rect>
</property>
<layout class="QVBoxLayout" name="taskProgressLayout">

View File

@ -39,7 +39,7 @@
#include <QProcess>
#include <memory>
#include <qglobal.h>
#include <QProgressDialog>
#include <sys.h>
#if defined Q_OS_WIN32
@ -106,7 +106,7 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt
#if defined Q_OS_WIN32
// taken from https://stackoverflow.com/a/25927081
// getting a proper output to console with redirection support on windows is apearently hell
// getting a proper output to console with redirection support on windows is apparently hell
void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr)
{
// Re-initialize the C runtime "FILE" handles with clean handles bound to "nul". We do this because it has been
@ -266,8 +266,11 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
m_prismExecutable = prism_executable;
auto prism_update_url = parser.value("update-url");
if (prism_update_url.isEmpty())
prism_update_url = BuildConfig.UPDATER_GITHUB_REPO;
if (prism_update_url.isEmpty())
prism_update_url = "https://github.com/PrismLauncher/PrismLauncher";
m_prismRepoUrl = QUrl::fromUserInput(prism_update_url);
m_checkOnly = parser.isSet("check-only");
@ -334,6 +337,8 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
#endif
}
m_updateLogPath = FS::PathCombine(m_dataPath, "prism_launcher_update.log");
{ // setup logging
static const QString logBase = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log";
auto moveFile = [](const QString& oldName, const QString& newName) {
@ -424,14 +429,16 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
if (adjustedBy.size()) {
qDebug() << "Work dir before adjustment : " << origCwdPath;
qDebug() << "Work dir after adjustment : " << QDir::currentPath();
qDebug() << "Data dir before adjustment : " << origCwdPath;
qDebug() << "Data dir after adjustment : " << m_dataPath;
qDebug() << "Adjusted by : " << adjustedBy;
} else {
qDebug() << "Work dir : " << QDir::currentPath();
qDebug() << "Data dir : " << QDir::currentPath();
}
qDebug() << "Work dir : " << QDir::currentPath();
qDebug() << "Binary path : " << binPath;
qDebug() << "Application root path : " << m_rootPath;
qDebug() << "Portable install : " << m_isPortable;
qDebug() << "<> Paths set.";
}
@ -442,14 +449,28 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
m_network->setProxy(proxy);
}
QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection);
auto marker_file_path = QDir(applicationDirPath()).absoluteFilePath(".prism_launcher_updater_unpack.marker");
auto marker_file = QFileInfo(marker_file_path);
if (marker_file.exists()) {
auto target_dir = QString(FS::read(marker_file_path)).trimmed();
if (target_dir.isEmpty()) {
qWarning() << "Empty updater marker file contains no install target. making best guess of parent dir";
target_dir = QDir(applicationDirPath()).absoluteFilePath("..");
}
QMetaObject::invokeMethod(
this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection);
} else {
QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection);
}
}
PrismUpdaterApp::~PrismUpdaterApp()
{
qDebug() << "updater shutting down";
// Shut down logger by setting the logger function to nothing
qInstallMessageHandler(nullptr);
qDebug() << "updater shutting down";
#if defined Q_OS_WIN32
// Detach from Windows console
@ -576,6 +597,81 @@ void PrismUpdaterApp::run()
exit(0);
}
void PrismUpdaterApp::moveAndFinishUpdate(QDir target)
{
logUpdate("Finishing update process");
auto manifest_path = FS::PathCombine(applicationDirPath(), "manifest.txt");
QFileInfo manifest(manifest_path);
auto app_dir = QDir(applicationDirPath());
QStringList file_list;
if (manifest.isFile()) {
// load manifest from file
logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath()));
try {
auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath()));
auto files = contents.split('\n');
for (auto file : files) {
file_list.append(file.trimmed());
}
} catch (FS::FileSystemException) {
}
}
if (file_list.isEmpty()) {
logUpdate(tr("Manifest empty, making best guess of the directory contents of %1").arg(applicationDirPath()));
auto entries = target.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
for (auto entry : entries) {
file_list.append(entry.fileName());
}
}
logUpdate(tr("Installing the following to %1 :\n %2").arg(target.absolutePath()).arg(file_list.join(",\n ")));
bool error = false;
QProgressDialog progress(tr("Backing up install at %1").arg(applicationDirPath()), "", 0, file_list.length());
progress.setCancelButton(nullptr);
progress.show();
QCoreApplication::processEvents();
int i = 0;
for (auto glob : file_list) {
QDirIterator iter(applicationDirPath(), QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
progress.setValue(i);
QCoreApplication::processEvents();
while (iter.hasNext()) {
auto to_install_file = iter.next();
auto rel_path = app_dir.relativeFilePath(to_install_file);
auto install_path = FS::PathCombine(target.absolutePath(), rel_path);
logUpdate(tr("Installing %1 from %2").arg(install_path).arg(to_install_file));
FS::ensureFilePathExists(install_path);
auto result = FS::copy(to_install_file, install_path)();
if (!result) {
error = true;
logUpdate(tr("Failed copy %1 to %2").arg(to_install_file).arg(install_path));
}
}
i++;
}
progress.setValue(i);
QCoreApplication::processEvents();
if (error) {
logUpdate(tr("There were errors installing the update."));
auto fail_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.fail");
FS::move(m_updateLogPath, fail_marker);
} else {
logUpdate(tr("Update succeed."));
auto success_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.success");
FS::move(m_updateLogPath, success_marker);
}
auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock");
FS::deletePath(update_lock_path);
exit(error ? 1 : 0);
}
void PrismUpdaterApp::printReleases()
{
for (auto release : m_releases) {
@ -739,24 +835,107 @@ bool PrismUpdaterApp::callAppImageUpdate()
void PrismUpdaterApp::clearUpdateLog()
{
auto update_log_path = FS::PathCombine(m_dataPath, "prism_launcher_update.log");
QFile::remove(update_log_path);
QFile::remove(m_updateLogPath);
}
void PrismUpdaterApp::logUpdate(const QString& msg)
{
qDebug() << qUtf8Printable(msg);
auto update_log_path = FS::PathCombine(m_dataPath, "prism_launcher_update.log");
FS::append(update_log_path, QStringLiteral("%1\n").arg(msg).toUtf8());
FS::append(m_updateLogPath, QStringLiteral("%1\n").arg(msg).toUtf8());
}
std::tuple<QDateTime, QString, QString, QString, QString> read_lock_File(const QString& path)
{
auto contents = QString(FS::read(path));
auto lines = contents.split('\n');
QDateTime timestamp;
QString from, to, target, data_path;
for (auto line : lines) {
auto index = line.indexOf("=");
if (index < 0)
continue;
auto left = line.left(index);
auto right = line.mid(index + 1);
if (left.toLower() == "timestamp") {
timestamp = QDateTime::fromString(right, Qt::ISODate);
} else if (left.toLower() == "from") {
from = right;
} else if (left.toLower() == "to") {
to = right;
} else if (left.toLower() == "target") {
target = right;
} else if (left.toLower() == "data_path") {
data_path = right;
}
}
return std::make_tuple(timestamp, from, to, target, data_path);
}
bool write_lock_file(const QString& path, QDateTime timestamp, QString from, QString to, QString target, QString data_path)
{
try {
FS::write(path, QStringLiteral("TIMESTAMP=%1\nFROM=%2\nTO=%3\nTARGET=%4\nDATA_PATH=%5\n")
.arg(timestamp.toString(Qt::ISODate))
.arg(from)
.arg(to)
.arg(target)
.arg(data_path)
.toUtf8());
} catch (FS::FileSystemException err) {
qWarning() << "Error writing lockfile:" << err.what() << "\n" << err.cause();
return false;
}
return true;
}
void PrismUpdaterApp::performInstall(QFileInfo file)
{
qDebug() << "starting install";
auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock");
FS::write(update_lock_path, QStringLiteral("FROM=%1\nTO=%2\n").arg(m_prismVersion).arg(m_install_release.tag_name).toUtf8());
QFileInfo update_lock(update_lock_path);
if (update_lock.exists()) {
auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock_path);
auto msg = tr("Update already in progress\n");
auto infoMsg =
tr("This installation has a update lock file present at: %1\n"
"\n"
"Timestamp: %2\n"
"Updating from version %3 to %4\n"
"Target install path: %5\n"
"Data Path: %6"
"\n"
"This likely means that a previous update attempt failed. Please ensure your installation is in working order before "
"proceeding.\n"
"Check the Prism Launcher updater log at \n"
"%7\n"
"for details on the last update attempt.\n"
"\n"
"To overwrite this lock and proceed with this update anyway, select \"Ignore\" below.")
.arg(update_lock_path)
.arg(timestamp.toString(Qt::ISODate), from, to, target, data_path)
.arg(m_updateLogPath);
QMessageBox msgBox;
msgBox.setText(msg);
msgBox.setInformativeText(infoMsg);
msgBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
switch (msgBox.exec()) {
case QMessageBox::Ignore:
break;
case QMessageBox::Cancel:
[[fallthrough]];
default:
return showFatalErrorMessage(tr("Update Aborted"), tr("The update attempt was aborted"));
}
}
write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, applicationDirPath(),
m_dataPath);
clearUpdateLog();
auto changelog_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.changelog");
FS::write(changelog_path, m_install_release.body.toUtf8());
logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name));
if (m_isPortable || file.suffix().toLower() == "zip") {
logUpdate(tr("Updating portable install at %1").arg(applicationDirPath()));
@ -779,14 +958,13 @@ void PrismUpdaterApp::unpackAndInstall(QFileInfo archive)
if (auto loc = unpackArchive(archive)) {
auto marker_file_path = loc.value().absoluteFilePath(".prism_launcher_updater_unpack.marker");
FS::write(marker_file_path, applicationDirPath().toUtf8());
auto new_updater_path = loc.value().absoluteFilePath("prismlauncher-updater");
auto new_updater_path = loc.value().absoluteFilePath("prismlauncher_updater");
#if defined Q_OS_WIN32
new_updater_path.append(".exe");
#endif
logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path));
QProcess proc = QProcess();
proc.startDetached(new_updater_path, {}, loc.value().absolutePath());
if (!proc.waitForStarted(5000)) {
if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) {
logUpdate(tr("Failed to launch '%1' %2").arg(new_updater_path).arg(proc.errorString()));
return exit(10);
}
@ -807,7 +985,10 @@ void PrismUpdaterApp::backupAppDir()
logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath()));
try {
auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath()));
file_list.append(contents.split('\n'));
auto files = contents.split('\n');
for (auto file : files) {
file_list.append(file.trimmed());
}
} catch (FS::FileSystemException) {
}
}
@ -815,19 +996,19 @@ void PrismUpdaterApp::backupAppDir()
if (file_list.isEmpty()) {
// best guess
if (BuildConfig.BUILD_PLATFORM.toLower() == "linux") {
file_list.append({ "PrismLauncher", "bin/*", "share/*", "lib/*" });
file_list.append({ "PrismLauncher", "bin", "share", "lib" });
} else { // windows by process of elimination
file_list.append({
"jars/*",
"jars",
"prismlauncher.exe",
"prismlauncher_filelink.exe",
"prismlauncher_updater.exe",
"qtlogging.ini",
"imageformats/*",
"iconengines/*",
"platforms/*",
"styles/*",
"styles/*",
"tls/*",
"imageformats",
"iconengines",
"platforms",
"styles",
"tls",
"qt.conf",
"Qt*.dll",
});
@ -839,32 +1020,41 @@ void PrismUpdaterApp::backupAppDir()
QDir app_dir = QCoreApplication::applicationDirPath();
auto backup_dir = FS::PathCombine(
app_dir.absolutePath(), QStringLiteral("backup_") +
QString(m_prismVersion).replace(QRegularExpression("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"), QString("_")) +
"-" + m_prismGitCommit);
app_dir.absolutePath(),
QStringLiteral("backup_") +
QString(m_prismVersion).replace(QRegularExpression("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"), QString("_")) + "-" +
m_prismGitCommit);
FS::ensureFolderPathExists(backup_dir);
auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt");
FS::write(backup_marker_path, backup_dir.toUtf8());
QProgressDialog progress(tr("Backing up install at %1").arg(applicationDirPath()), "", 0, file_list.length());
progress.setCancelButton(nullptr);
progress.show();
QCoreApplication::processEvents();
int i = 0;
for (auto glob : file_list) {
QDirIterator iter(app_dir.absolutePath(), QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot,
QDirIterator::Subdirectories);
QDirIterator iter(app_dir.absolutePath(), QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
progress.setValue(i);
QCoreApplication::processEvents();
while (iter.hasNext()) {
auto to_bak_file = iter.next();
auto rel_path = app_dir.relativeFilePath(to_bak_file);
auto bak_path = FS::PathCombine(backup_dir, rel_path);
if (QFileInfo(to_bak_file).isFile()) {
logUpdate(tr("Backing up and then removing %1").arg(to_bak_file));
FS::ensureFilePathExists(bak_path);
auto result = FS::copy(to_bak_file, bak_path)();
if (!result) {
logUpdate(tr("Failed to backup %1 to %2").arg(to_bak_file).arg(bak_path));
} else {
if (!FS::deletePath(to_bak_file))
logUpdate(tr("Failed to remove %1").arg(to_bak_file));
}
logUpdate(tr("Backing up and then removing %1").arg(to_bak_file));
FS::ensureFilePathExists(bak_path);
auto result = FS::copy(to_bak_file, bak_path)();
if (!result) {
logUpdate(tr("Failed to backup %1 to %2").arg(to_bak_file).arg(bak_path));
} else {
if (!FS::deletePath(to_bak_file))
logUpdate(tr("Failed to remove %1").arg(to_bak_file));
}
}
i++;
}
progress.setValue(i);
QCoreApplication::processEvents();
}
std::optional<QDir> PrismUpdaterApp::unpackArchive(QFileInfo archive)

View File

@ -85,7 +85,7 @@ class PrismUpdaterApp : public QApplication {
QFileInfo downloadAsset(const GitHubReleaseAsset& asset);
bool callAppImageUpdate();
void moveAndPostProcess(QDir target);
void moveAndFinishUpdate(QDir target);
public slots:
void downloadError(QString reason);
@ -98,7 +98,6 @@ class PrismUpdaterApp : public QApplication {
void clearUpdateLog();
void logUpdate(const QString& msg);
QString m_rootPath;
QString m_dataPath;
bool m_isPortable = false;
@ -114,6 +113,8 @@ class PrismUpdaterApp : public QApplication {
bool m_selectUI;
bool m_allowDowngrade;
QString m_updateLogPath;
QString m_prismBinaryName;
QString m_prismVersion;
int m_prismVersionMajor = -1;

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>478</width>
<height>517</height>
<width>468</width>
<height>385</height>
</rect>
</property>
<property name="windowTitle">