From 69be23c5f629884465dec98efcb9d5a2678b4df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 18 Nov 2016 16:04:08 +0100 Subject: GH-1726 better failure detection for updates Instead of just checking if the new version started, make sure it is able to write its IPC key to a file and then use the key to connect to the process. --- api/logic/updater/DownloadTask_test.cpp | 4 +- api/logic/updater/GoUpdate.h | 21 +- application/CMakeLists.txt | 2 + application/MainWindow.cpp | 4 +- application/MultiMC.cpp | 340 ++----------------------- application/MultiMC.h | 4 +- application/UpdateController.cpp | 431 ++++++++++++++++++++++++++++++++ application/UpdateController.h | 44 ++++ application/main.cpp | 22 ++ libraries/LocalPeer/include/LocalPeer.h | 5 +- libraries/LocalPeer/src/LocalPeer.cpp | 5 + 11 files changed, 553 insertions(+), 329 deletions(-) create mode 100644 application/UpdateController.cpp create mode 100644 application/UpdateController.h diff --git a/api/logic/updater/DownloadTask_test.cpp b/api/logic/updater/DownloadTask_test.cpp index 5b88b366..e75c3ffa 100644 --- a/api/logic/updater/DownloadTask_test.cpp +++ b/api/logic/updater/DownloadTask_test.cpp @@ -52,8 +52,8 @@ QDebug operator<<(QDebug dbg, const Operation::Type &t) QDebug operator<<(QDebug dbg, const Operation &u) { - dbg.nospace() << "Operation(type=" << u.type << " file=" << u.file - << " dest=" << u.dest << " mode=" << u.mode << ")"; + dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source + << " dest=" << u.destination << " mode=" << u.destinationMode << ")"; return dbg.maybeSpace(); } diff --git a/api/logic/updater/GoUpdate.h b/api/logic/updater/GoUpdate.h index b8a534de..95f26b8c 100644 --- a/api/logic/updater/GoUpdate.h +++ b/api/logic/updater/GoUpdate.h @@ -68,19 +68,22 @@ typedef QList VersionFileList; */ struct MULTIMC_LOGIC_EXPORT Operation { - static Operation CopyOp(QString fsource, QString fdest, int fmode=0644) + static Operation CopyOp(QString from, QString to, int fmode=0644) { - return Operation{OP_REPLACE, fsource, fdest, fmode}; + return Operation{OP_REPLACE, from, to, fmode}; } static Operation DeleteOp(QString file) { - return Operation{OP_DELETE, file, "", 0644}; + return Operation{OP_DELETE, QString(), file, 0644}; } // FIXME: for some types, some of the other fields are irrelevant! bool operator==(const Operation &u2) const { - return type == u2.type && file == u2.file && dest == u2.dest && mode == u2.mode; + return type == u2.type && + source == u2.source && + destination == u2.destination && + destinationMode == u2.destinationMode; } //! Specifies the type of operation that this is. @@ -90,14 +93,14 @@ struct MULTIMC_LOGIC_EXPORT Operation OP_DELETE, } type; - //! The file to operate on. - QString file; + //! The source file, if any + QString source; //! The destination file. - QString dest; + QString destination; - //! The mode to change the source file to. - int mode; + //! The mode to change the destination file to. + int destinationMode; }; typedef QList OperationList; diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 64879e27..18351cce 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -85,6 +85,8 @@ SET(MULTIMC_SOURCES MultiMC.cpp BuildConfig.h ${PROJECT_BINARY_DIR}/BuildConfig.cpp + UpdateController.cpp + UpdateController.h # GUI - general utilities GuiUtil.h diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index e5ea81f6..4e649521 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -90,6 +90,7 @@ #include "dialogs/ExportInstanceDialog.h" #include #include +#include "UpdateController.h" class MainWindow::Ui { @@ -952,7 +953,8 @@ void MainWindow::downloadUpdates(GoUpdate::Status status) // If the task succeeds, install the updates. if (updateDlg.execWithTask(&updateTask)) { - MMC->installUpdates(updateTask.updateFilesDir(), updateTask.operations()); + UpdateController update(this, MMC->root(), updateTask.updateFilesDir(), updateTask.operations()); + update.installUpdates(); } else { diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index c102e198..64c380ec 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -73,6 +73,8 @@ #include #endif +static const QLatin1String liveCheckFile("live.check"); + using namespace Commandline; MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) @@ -132,6 +134,9 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) parser.addOption("launch"); parser.addShortOpt("launch", 'l'); parser.addDocumentation("launch", "launch the specified instance (by instance ID)"); + // --alive + parser.addSwitch("alive"); + parser.addDocumentation("alive", "write a small '" + liveCheckFile + "' file after MultiMC starts"); // parse the arguments try @@ -165,6 +170,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) } } m_instanceIdToLaunch = args["launch"].toString(); + m_liveCheck = args["alive"].toBool(); QString origcwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); @@ -242,6 +248,27 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; } + do // once + { + if(m_liveCheck) + { + QFile check(liveCheckFile); + if(!check.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + qWarning() << "Could not open" << liveCheckFile << "for writing!"; + break; + } + auto payload = appID.toString().toUtf8(); + if(check.write(payload) != payload.size()) + { + qWarning() << "Could not write into" << liveCheckFile; + check.remove(); + break; + } + check.close(); + } + } while(false); + // load settings initGlobalSettings(); @@ -697,319 +724,6 @@ std::shared_ptr MultiMC::javalist() return m_javalist; } -// from -#ifndef S_IRUSR -#define __S_IREAD 0400 /* Read by owner. */ -#define __S_IWRITE 0200 /* Write by owner. */ -#define __S_IEXEC 0100 /* Execute by owner. */ -#define S_IRUSR __S_IREAD /* Read by owner. */ -#define S_IWUSR __S_IWRITE /* Write by owner. */ -#define S_IXUSR __S_IEXEC /* Execute by owner. */ - -#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */ -#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */ -#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */ - -#define S_IROTH (S_IRGRP >> 3) /* Read by others. */ -#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */ -#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */ -#endif -static QFile::Permissions unixModeToPermissions(const int mode) -{ - QFile::Permissions perms; - - if (mode & S_IRUSR) - { - perms |= QFile::ReadUser; - } - if (mode & S_IWUSR) - { - perms |= QFile::WriteUser; - } - if (mode & S_IXUSR) - { - perms |= QFile::ExeUser; - } - - if (mode & S_IRGRP) - { - perms |= QFile::ReadGroup; - } - if (mode & S_IWGRP) - { - perms |= QFile::WriteGroup; - } - if (mode & S_IXGRP) - { - perms |= QFile::ExeGroup; - } - - if (mode & S_IROTH) - { - perms |= QFile::ReadOther; - } - if (mode & S_IWOTH) - { - perms |= QFile::WriteOther; - } - if (mode & S_IXOTH) - { - perms |= QFile::ExeOther; - } - return perms; -} - -void MultiMC::installUpdates(const QString updateFilesDir, GoUpdate::OperationList operations) -{ - qint64 pid = -1; - QStringList args; - bool started = false; - - qDebug() << "Installing updates."; -#ifdef Q_OS_WIN - QString finishCmd = applicationFilePath(); -#elif defined Q_OS_LINUX - QString finishCmd = FS::PathCombine(root(), "MultiMC"); -#elif defined Q_OS_MAC - QString finishCmd = applicationFilePath(); -#else -#error Unsupported operating system. -#endif - - QString backupPath = FS::PathCombine(root(), "update", "backup"); - QDir origin(root()); - - // clean up the backup folder. it should be empty before we start - if(!FS::deletePath(backupPath)) - { - qWarning() << "couldn't remove previous backup folder" << backupPath; - } - // and it should exist. - if(!FS::ensureFolderPathExists(backupPath)) - { - qWarning() << "couldn't create folder" << backupPath; - return; - } - - struct BackupEntry - { - QString orig; - QString backup; - }; - enum Failure - { - Replace, - Delete, - Start, - Nothing - } failedOperationType = Nothing; - QString failedFile; - - QList backups; - QList trashcan; - - bool useXPHack = false; - QString exePath; - QString exeOrigin; - QString exeBackup; - - // perform the update operations - for(auto op: operations) - { - switch(op.type) - { - // replace = move original out to backup, if it exists, move the new file in its place - case GoUpdate::Operation::OP_REPLACE: - { -#ifdef Q_OS_WIN32 - // hack for people renaming the .exe because ... reasons :) - if(op.dest == "MultiMC.exe") - { - op.dest = QFileInfo(applicationFilePath()).fileName(); - } -#endif - QFileInfo replaced (FS::PathCombine(root(), op.dest)); -#ifdef Q_OS_WIN32 - if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA) - { - if(replaced.fileName() == "MultiMC.exe") - { - QDir rootDir(root()); - exeOrigin = rootDir.relativeFilePath(op.file); - exePath = rootDir.relativeFilePath(op.dest); - exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, replaced.fileName())); - useXPHack = true; - continue; - } - } -#endif - if(replaced.exists()) - { - QString backupName = op.dest; - backupName.replace('/', '_'); - QString backupFilePath = FS::PathCombine(backupPath, backupName); - if(!QFile::rename(replaced.absoluteFilePath(), backupFilePath)) - { - qWarning() << "Couldn't move:" << replaced.absoluteFilePath() << "to" << backupFilePath; - failedOperationType = Replace; - failedFile = op.dest; - goto FAILED; - } - BackupEntry be; - be.orig = replaced.absoluteFilePath(); - be.backup = backupFilePath; - backups.append(be); - } - // make sure the folder we are putting this into exists - if(!FS::ensureFilePathExists(replaced.absoluteFilePath())) - { - qWarning() << "REPLACE: Couldn't create folder:" << replaced.absoluteFilePath(); - failedOperationType = Replace; - failedFile = op.dest; - goto FAILED; - } - // now move the new file in - if(!QFile::rename(op.file, replaced.absoluteFilePath())) - { - qWarning() << "REPLACE: Couldn't move:" << op.file << "to" << replaced.absoluteFilePath(); - failedOperationType = Replace; - failedFile = op.dest; - goto FAILED; - } - QFile::setPermissions(replaced.absoluteFilePath(), unixModeToPermissions(op.mode)); - } - break; - // delete = move original to backup - case GoUpdate::Operation::OP_DELETE: - { - QString origFilePath = FS::PathCombine(root(), op.file); - if(QFile::exists(origFilePath)) - { - QString backupName = op.file; - backupName.replace('/', '_'); - QString trashFilePath = FS::PathCombine(backupPath, backupName); - - if(!QFile::rename(origFilePath, trashFilePath)) - { - qWarning() << "DELETE: Couldn't move:" << op.file << "to" << trashFilePath; - failedFile = op.file; - failedOperationType = Delete; - goto FAILED; - } - BackupEntry be; - be.orig = origFilePath; - be.backup = trashFilePath; - trashcan.append(be); - } - } - break; - } - } - - // try to start the new binary - args = qApp->arguments(); - args.removeFirst(); - - // on old Windows, do insane things... no error checking here, this is just to have something. - if(useXPHack) - { - QString script; - auto nativePath = QDir::toNativeSeparators(exePath); - auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin); - auto nativeBackupPath = QDir::toNativeSeparators(exeBackup); - - // so we write this vbscript thing... - QTextStream out(&script); - out << "WScript.Sleep 1000\n"; - out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n"; - out << "Set shell=CreateObject(\"WScript.Shell\")\n"; - out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n"; - out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n"; - out << "shell.Run \"" << nativePath << "\"\n"; - - QString scriptPath = FS::PathCombine(root(), "update", "update.vbs"); - - // we save it - QFile scriptFile(scriptPath); - scriptFile.open(QIODevice::WriteOnly); - scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n")); - scriptFile.close(); - - // we run it - started = QProcess::startDetached("wscript", {scriptPath}, root()); - - // and we quit. conscious thought. - qApp->quit(); - return; - } - started = QProcess::startDetached(finishCmd, args, QDir::currentPath(), &pid); - // failed to start... ? - if(!started || pid == -1) - { - qWarning() << "Couldn't start new process properly!"; - failedOperationType = Start; - goto FAILED; - } - origin.rmdir(updateFilesDir); - qApp->quit(); - return; - -FAILED: - qWarning() << "Update failed!"; - bool revertOK = true; - // if the above failed, roll back changes - for(auto backup:backups) - { - qWarning() << "restoring" << backup.orig << "from" << backup.backup; - if(!QFile::remove(backup.orig)) - { - revertOK = false; - qWarning() << "removing new" << backup.orig << "failed!"; - continue; - } - - if(!QFile::rename(backup.backup, backup.orig)) - { - revertOK = false; - qWarning() << "restoring" << backup.orig << "failed!"; - } - } - for(auto backup:trashcan) - { - qWarning() << "restoring" << backup.orig << "from" << backup.backup; - if(!QFile::rename(backup.backup, backup.orig)) - { - revertOK = false; - qWarning() << "restoring" << backup.orig << "failed!"; - } - } - QString msg; - if(!revertOK) - { - msg = tr("The update failed and then the update revert failed too.\n" - "You will have to repair MultiMC manually.\n" - "Please let us know why and how this happened.").arg(failedFile); - } - else switch (failedOperationType) - { - case Replace: - msg = tr("Couldn't replace file %1. Changes were reverted.\n" - "See the MultiMC log file for details.").arg(failedFile); - break; - case Delete: - msg = tr("Couldn't remove file %1. Changes were reverted.\n" - "See the MultiMC log file for details.").arg(failedFile); - break; - case Start: - msg = tr("The new version didn't start and the update was rolled back."); - break; - case Nothing: - default: - return; - } - QMessageBox::critical(nullptr, tr("Update failed!"), msg); -} - std::vector MultiMC::getValidApplicationThemes() { std::vector ret; diff --git a/application/MultiMC.h b/application/MultiMC.h index c416b8c0..23cafdb1 100644 --- a/application/MultiMC.h +++ b/application/MultiMC.h @@ -132,9 +132,6 @@ public: return m_rootPath; } - // install updates now. - void installUpdates(const QString updateFilesDir, GoUpdate::OperationList operations); - /*! * Opens a json file using either a system default editor, or, if not empty, the editor * specified in the settings @@ -223,5 +220,6 @@ private: LocalPeer * m_peerInstance = nullptr; public: QString m_instanceIdToLaunch; + bool m_liveCheck = false; std::unique_ptr logFile; }; diff --git a/application/UpdateController.cpp b/application/UpdateController.cpp new file mode 100644 index 00000000..e3d8f2fb --- /dev/null +++ b/application/UpdateController.cpp @@ -0,0 +1,431 @@ +#include +#include +#include +#include +#include "UpdateController.h" +#include +#include +#include +#include + +// from +#ifndef S_IRUSR +#define __S_IREAD 0400 /* Read by owner. */ +#define __S_IWRITE 0200 /* Write by owner. */ +#define __S_IEXEC 0100 /* Execute by owner. */ +#define S_IRUSR __S_IREAD /* Read by owner. */ +#define S_IWUSR __S_IWRITE /* Write by owner. */ +#define S_IXUSR __S_IEXEC /* Execute by owner. */ + +#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */ +#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */ +#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */ + +#define S_IROTH (S_IRGRP >> 3) /* Read by others. */ +#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */ +#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */ +#endif +static QFile::Permissions unixModeToPermissions(const int mode) +{ + QFile::Permissions perms; + + if (mode & S_IRUSR) + { + perms |= QFile::ReadUser; + } + if (mode & S_IWUSR) + { + perms |= QFile::WriteUser; + } + if (mode & S_IXUSR) + { + perms |= QFile::ExeUser; + } + + if (mode & S_IRGRP) + { + perms |= QFile::ReadGroup; + } + if (mode & S_IWGRP) + { + perms |= QFile::WriteGroup; + } + if (mode & S_IXGRP) + { + perms |= QFile::ExeGroup; + } + + if (mode & S_IROTH) + { + perms |= QFile::ReadOther; + } + if (mode & S_IWOTH) + { + perms |= QFile::WriteOther; + } + if (mode & S_IXOTH) + { + perms |= QFile::ExeOther; + } + return perms; +} + +static const QLatin1String liveCheckFile("live.check"); + +UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations) +{ + m_parent = parent; + m_root = root; + m_updateFilesDir = updateFilesDir; + m_operations = operations; +} + + +void UpdateController::installUpdates() +{ + qint64 pid = -1; + QStringList args; + bool started = false; + + qDebug() << "Installing updates."; +#ifdef Q_OS_WIN + QString finishCmd = QApplication::applicationFilePath(); +#elif defined Q_OS_LINUX + QString finishCmd = FS::PathCombine(m_root, "MultiMC"); +#elif defined Q_OS_MAC + QString finishCmd = QApplication::applicationFilePath(); +#else +#error Unsupported operating system. +#endif + + QString backupPath = FS::PathCombine(m_root, "update", "backup"); + QDir origin(m_root); + + // clean up the backup folder. it should be empty before we start + if(!FS::deletePath(backupPath)) + { + qWarning() << "couldn't remove previous backup folder" << backupPath; + } + // and it should exist. + if(!FS::ensureFolderPathExists(backupPath)) + { + qWarning() << "couldn't create folder" << backupPath; + return; + } + + bool useXPHack = false; + QString exePath; + QString exeOrigin; + QString exeBackup; + + // perform the update operations + for(auto op: m_operations) + { + switch(op.type) + { + // replace = move original out to backup, if it exists, move the new file in its place + case GoUpdate::Operation::OP_REPLACE: + { +#ifdef Q_OS_WIN32 + // hack for people renaming the .exe because ... reasons :) + if(op.destination == "MultiMC.exe") + { + op.destination = QFileInfo(QApplication::applicationFilePath()).fileName(); + } +#endif + QFileInfo destination (FS::PathCombine(m_root, op.destination)); +#ifdef Q_OS_WIN32 + if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA) + { + if(destination.fileName() == "MultiMC.exe") + { + QDir rootDir(m_root); + exeOrigin = rootDir.relativeFilePath(op.source); + exePath = rootDir.relativeFilePath(op.destination); + exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, destination.fileName())); + useXPHack = true; + continue; + } + } +#endif + if(destination.exists()) + { + QString backupName = op.destination; + backupName.replace('/', '_'); + QString backupFilePath = FS::PathCombine(backupPath, backupName); + if(!QFile::rename(destination.absoluteFilePath(), backupFilePath)) + { + qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath; + m_failedOperationType = Replace; + m_failedFile = op.destination; + fail(); + return; + } + BackupEntry be; + be.original = destination.absoluteFilePath(); + be.backup = backupFilePath; + be.update = op.source; + m_replace_backups.append(be); + } + // make sure the folder we are putting this into exists + if(!FS::ensureFilePathExists(destination.absoluteFilePath())) + { + qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath(); + m_failedOperationType = Replace; + m_failedFile = op.destination; + fail(); + return; + } + // now move the new file in + if(!QFile::rename(op.source, destination.absoluteFilePath())) + { + qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath(); + m_failedOperationType = Replace; + m_failedFile = op.destination; + fail(); + return; + } + QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode)); + } + break; + // delete = move original to backup + case GoUpdate::Operation::OP_DELETE: + { + QString destFilePath = FS::PathCombine(m_root, op.destination); + if(QFile::exists(destFilePath)) + { + QString backupName = op.destination; + backupName.replace('/', '_'); + QString trashFilePath = FS::PathCombine(backupPath, backupName); + + if(!QFile::rename(destFilePath, trashFilePath)) + { + qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath; + m_failedFile = op.destination; + m_failedOperationType = Delete; + fail(); + return; + } + BackupEntry be; + be.original = destFilePath; + be.backup = trashFilePath; + m_delete_backups.append(be); + } + } + break; + } + } + + // try to start the new binary + args = qApp->arguments(); + args.removeFirst(); + + // on old Windows, do insane things... no error checking here, this is just to have something. + if(useXPHack) + { + QString script; + auto nativePath = QDir::toNativeSeparators(exePath); + auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin); + auto nativeBackupPath = QDir::toNativeSeparators(exeBackup); + + // so we write this vbscript thing... + QTextStream out(&script); + out << "WScript.Sleep 1000\n"; + out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n"; + out << "Set shell=CreateObject(\"WScript.Shell\")\n"; + out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n"; + out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n"; + out << "shell.Run \"" << nativePath << "\"\n"; + + QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs"); + + // we save it + QFile scriptFile(scriptPath); + scriptFile.open(QIODevice::WriteOnly); + scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n")); + scriptFile.close(); + + // we run it + started = QProcess::startDetached("wscript", {scriptPath}, m_root); + + // and we quit. conscious thought. + qApp->quit(); + return; + } + bool doLiveCheck = true; + bool startFailed = false; + + // remove live check file, if any + if(QFile::exists(liveCheckFile)) + { + if(!QFile::remove(liveCheckFile)) + { + qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :("; + doLiveCheck = false; + } + } + + if(doLiveCheck) + { + if(!args.contains("--alive")) + { + args.append("--alive"); + } + } + + // start the updated application + started = QProcess::startDetached(finishCmd, args, QDir::currentPath(), &pid); + // much dumber check - just find out if the call + if(!started || pid == -1) + { + qWarning() << "Couldn't start new process properly!"; + startFailed = true; + } + if(!startFailed && doLiveCheck) + { + int attempts = 0; + while(attempts < 10) + { + attempts++; + QString key; + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + if(!QFile::exists(liveCheckFile)) + { + qWarning() << "Couldn't find the" << liveCheckFile << "file!"; + startFailed = true; + continue; + } + try + { + key = QString::fromUtf8(FS::read(liveCheckFile)); + auto id = ApplicationId::fromRawString(key); + LocalPeer peer(nullptr, id); + if(peer.isClient()) + { + startFailed = false; + qDebug() << "Found process started with key " << key; + break; + } + else + { + startFailed = true; + qDebug() << "Process started with key " << key << "apparently died or is not reponding..."; + break; + } + } + catch(Exception e) + { + qWarning() << "Couldn't read the" << liveCheckFile << "file!"; + startFailed = true; + continue; + } + } + } + if(startFailed) + { + m_failedOperationType = Start; + fail(); + return; + } + else + { + origin.rmdir(m_updateFilesDir); + qApp->quit(); + return; + } +} + +void UpdateController::fail() +{ + qWarning() << "Update failed!"; + + QString msg; + bool doRollback = false; + QString failTitle = QObject::tr("Update failed!"); + QString rollFailTitle = QObject::tr("Rollback failed!"); + switch (m_failedOperationType) + { + case Replace: + { + msg = QObject::tr("Couldn't replace file %1. Changes will be reverted.\n" + "See the MultiMC log file for details.").arg(m_failedFile); + doRollback = true; + QMessageBox::critical(m_parent, failTitle, msg); + break; + } + case Delete: + { + msg = QObject::tr("Couldn't remove file %1. Changes will be reverted.\n" + "See the MultiMC log file for details.").arg(m_failedFile); + doRollback = true; + QMessageBox::critical(m_parent, failTitle, msg); + break; + } + case Start: + { + msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n" + "\n" + "Roll back to previous version?"); + auto result = QMessageBox::critical( + m_parent, + failTitle, + msg, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes + ); + doRollback = (result == QMessageBox::Yes); + break; + } + case Nothing: + default: + return; + } + if(doRollback) + { + auto rollbackOK = rollback(); + if(!rollbackOK) + { + msg = QObject::tr("The rollback failed too.\n" + "You will have to repair MultiMC manually.\n" + "Please let us know why and how this happened.").arg(m_failedFile); + QMessageBox::critical(m_parent, rollFailTitle, msg); + qApp->quit(); + } + } + else + { + qApp->quit(); + } +} + +bool UpdateController::rollback() +{ + bool revertOK = true; + // if the above failed, roll back changes + for(auto backup:m_replace_backups) + { + qWarning() << "restoring" << backup.original << "from" << backup.backup; + if(!QFile::rename(backup.original, backup.update)) + { + revertOK = false; + qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!"; + continue; + } + + if(!QFile::rename(backup.backup, backup.original)) + { + revertOK = false; + qWarning() << "restoring" << backup.original << "failed!"; + } + } + for(auto backup:m_delete_backups) + { + qWarning() << "restoring" << backup.original << "from" << backup.backup; + if(!QFile::rename(backup.backup, backup.original)) + { + revertOK = false; + qWarning() << "restoring" << backup.original << "failed!"; + } + } + return revertOK; +} diff --git a/application/UpdateController.h b/application/UpdateController.h new file mode 100644 index 00000000..2b258701 --- /dev/null +++ b/application/UpdateController.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +class QWidget; + +class UpdateController +{ +public: + UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations); + void installUpdates(); + +private: + void fail(); + bool rollback(); + +private: + QString m_root; + QString m_updateFilesDir; + GoUpdate::OperationList m_operations; + QWidget * m_parent; + + struct BackupEntry + { + // path where we got the new file from + QString update; + // path of what is being actually updated + QString original; + // path where the backup of the updated file was placed + QString backup; + }; + QList m_replace_backups; + QList m_delete_backups; + enum Failure + { + Replace, + Delete, + Start, + Nothing + } m_failedOperationType = Nothing; + QString m_failedFile; +}; diff --git a/application/main.cpp b/application/main.cpp index 61771f6e..0fd9f089 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -4,8 +4,30 @@ #include #include +// #define BREAK_INFINITE_LOOP +// #define BREAK_EXCEPTION +// #define BREAK_RETURN + +#ifdef BREAK_INFINITE_LOOP +#include +#include +#endif + int main(int argc, char *argv[]) { +#ifdef BREAK_INFINITE_LOOP + while(true) + { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } +#endif +#ifdef BREAK_EXCEPTION + throw 42; +#endif +#ifdef BREAK_RETURN + return 42; +#endif + // initialize Qt MultiMC app(argc, argv); diff --git a/libraries/LocalPeer/include/LocalPeer.h b/libraries/LocalPeer/include/LocalPeer.h index 940bfcbd..7558f18e 100644 --- a/libraries/LocalPeer/include/LocalPeer.h +++ b/libraries/LocalPeer/include/LocalPeer.h @@ -54,8 +54,11 @@ public: /* methods */ static ApplicationId fromTraditionalApp(); // ID based on a path with all the application data (no two instances with the same data path should run) static ApplicationId fromPathAndVersion(const QString & dataPath, const QString & version); - // fully custom ID + // custom ID static ApplicationId fromCustomId(const QString & id); + // custom ID, based on a raw string previously acquired from 'toString' + static ApplicationId fromRawString(const QString & id); + QString toString() { diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index db0c73e5..f71c62d8 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -108,6 +108,11 @@ ApplicationId ApplicationId::fromCustomId(const QString& id) return ApplicationId(QLatin1String("qtsingleapp-") + id); } +ApplicationId ApplicationId::fromRawString(const QString& id) +{ + return ApplicationId(id); +} + LocalPeer::LocalPeer(QObject * parent, const ApplicationId &appId) : QObject(parent), id(appId) { -- cgit v1.2.3