diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index 933c4dbc..8c827806 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -992,7 +992,7 @@ void MainWindow::downloadUpdates(GoUpdate::Status status) // If the task succeeds, install the updates. if (updateDlg.exec(&updateTask)) { - MMC->installUpdates(updateTask.updateFilesDir()); + MMC->installUpdates(updateTask.updateFilesDir(), updateTask.operations()); } else { diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index 67b50b40..a21455f0 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -583,7 +583,69 @@ std::shared_ptr MultiMC::javalist() return m_javalist; } -void MultiMC::installUpdates(const QString updateFilesDir) +// 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) { qDebug() << "Installing updates."; #ifdef WINDOWS @@ -596,14 +658,96 @@ void MultiMC::installUpdates(const QString updateFilesDir) #error Unsupported operating system. #endif - QStringList args; - args << "--install-dir" << root(); - args << "--package-dir" << updateFilesDir; - args << "--script" << PathCombine(updateFilesDir, "file_list.xml"); - args << "--wait" << QString::number(applicationPid()); - args << "--finish-cmd" << finishCmd; - args << "--finish-dir" << dataPath; - qDebug() << "Running updater with args" << args.join(" "); + QString backupPath = PathCombine(root(), "update-backup"); + QString trashPath = PathCombine(root(), "update-trash"); + if(!ensureFolderPathExists(backupPath)) + { + qWarning() << "couldn't create folder" << backupPath; + return; + } + if(!ensureFolderPathExists(trashPath)) + { + qWarning() << "couldn't create folder" << trashPath; + return; + } + struct BackupEntry + { + QString orig; + QString backup; + }; + QList backups; + QList trashcan; + for(auto op: operations) + { + switch(op.type) + { + case GoUpdate::Operation::OP_COPY: + { + QFileInfo replaced (PathCombine(root(), op.dest)); + if(replaced.exists()) + { + QString backupFilePath = PathCombine(backupPath, replaced.completeBaseName()); + QFile::rename(replaced.absoluteFilePath(), backupFilePath); + BackupEntry be; + be.orig = replaced.absoluteFilePath(); + be.backup = backupFilePath; + backups.append(be); + } + QFile::copy(op.file, replaced.absoluteFilePath()); + QFile::setPermissions(replaced.absoluteFilePath(), unixModeToPermissions(op.mode)); + } + break; + case GoUpdate::Operation::OP_DELETE: + { + QString trashFilePath = PathCombine(backupPath, op.file); + QString origFilePath = PathCombine(root(), op.file); + if(QFile::exists(origFilePath)) + { + QFile::rename(origFilePath, trashFilePath); + BackupEntry be; + be.orig = origFilePath; + be.backup = trashFilePath; + trashcan.append(be); + } + } + break; + } + } + + // try to start the new binary + qint64 pid = -1; + auto args = qApp->arguments(); + args.removeFirst(); + QProcess::startDetached(finishCmd, args, QDir::currentPath(), &pid); + // failed to start... ? + if(pid == -1) + { + goto FAILED; + } + // now clean up the backed up stuff. + for(auto backup:backups) + { + QFile::remove(backup.backup); + } + for(auto backup:trashcan) + { + QFile::remove(backup.backup); + } + qApp->quit(); + return; + +FAILED: + // if the above failed, roll back changes + for(auto backup:backups) + { + QFile::remove(backup.orig); + QFile::rename(backup.backup, backup.orig); + } + for(auto backup:trashcan) + { + QFile::rename(backup.backup, backup.orig); + } + // and do nothing } void MultiMC::setIconTheme(const QString& name) diff --git a/application/MultiMC.h b/application/MultiMC.h index f815b8e4..9edf0596 100644 --- a/application/MultiMC.h +++ b/application/MultiMC.h @@ -6,6 +6,7 @@ #include #include #include +#include class QFile; class MinecraftVersionList; @@ -105,7 +106,7 @@ public: } // APPLICATION ONLY - void installUpdates(const QString updateFilesDir); + void installUpdates(const QString updateFilesDir, GoUpdate::OperationList operations); /*! * Opens a json file using either a system default editor, or, if note empty, the editor diff --git a/logic/updater/DownloadTask.cpp b/logic/updater/DownloadTask.cpp index 352f1f0d..4a42f583 100644 --- a/logic/updater/DownloadTask.cpp +++ b/logic/updater/DownloadTask.cpp @@ -89,7 +89,6 @@ void DownloadTask::processDownloadedVersionInfo() { VersionFileList m_currentVersionFileList; VersionFileList m_newVersionFileList; - OperationList operationList; setStatus(tr("Reading file list for new version...")); qDebug() << "Reading file list for new version..."; @@ -125,19 +124,12 @@ void DownloadTask::processDownloadedVersionInfo() NetJobPtr netJob (new NetJob("Update Files")); // fill netJob and operationList - if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, operationList)) + if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations)) { emitFailed(tr("Failed to process update lists...")); return; } - // write the instruction file for the file swapper - if(!writeInstallScript(operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml"))) - { - emitFailed(tr("Failed to write update script file.")); - return; - } - // Now start the download. QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished); QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged); @@ -170,4 +162,9 @@ QString DownloadTask::updateFilesDir() return m_updateFilesDir.path(); } +OperationList DownloadTask::operations() +{ + return m_operations; +} + } \ No newline at end of file diff --git a/logic/updater/DownloadTask.h b/logic/updater/DownloadTask.h index 197aa3e6..3bc504fc 100644 --- a/logic/updater/DownloadTask.h +++ b/logic/updater/DownloadTask.h @@ -35,6 +35,9 @@ public: /// Get the directory that will contain the update files. QString updateFilesDir(); + /// Get the list of operations that should be done + OperationList operations(); + /// set updater download behavior void setUseLocalUpdater(bool useLocal); @@ -61,6 +64,8 @@ protected: Status m_status; + OperationList m_operations; + /*! * Temporary directory to store update files in. * This will be set to not auto delete. Task will fail if this fails to be created. diff --git a/logic/updater/GoUpdate.cpp b/logic/updater/GoUpdate.cpp index d85f00d6..a43c5e7c 100644 --- a/logic/updater/GoUpdate.cpp +++ b/logic/updater/GoUpdate.cpp @@ -213,78 +213,4 @@ bool fixPathForOSX(QString &path) return false; } } - -bool writeInstallScript(OperationList &opsList, QString scriptFile) -{ - // Build the base structure of the XML document. - QDomDocument doc; - - QDomElement root = doc.createElement("update"); - root.setAttribute("version", "3"); - doc.appendChild(root); - - QDomElement installFiles = doc.createElement("install"); - root.appendChild(installFiles); - - QDomElement removeFiles = doc.createElement("uninstall"); - root.appendChild(removeFiles); - - // Write the operation list to the XML document. - for (Operation op : opsList) - { - QDomElement file = doc.createElement("file"); - - switch (op.type) - { - case Operation::OP_COPY: - { - // Install the file. - QDomElement name = doc.createElement("source"); - QDomElement path = doc.createElement("dest"); - QDomElement mode = doc.createElement("mode"); - name.appendChild(doc.createTextNode(op.file)); - path.appendChild(doc.createTextNode(op.dest)); - // We need to add a 0 at the beginning here, because Qt doesn't convert to octal - // correctly. - mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8))); - file.appendChild(name); - file.appendChild(path); - file.appendChild(mode); - installFiles.appendChild(file); - qDebug() << "Will install file " << op.file << " to " << op.dest; - } - break; - - case Operation::OP_DELETE: - { - // Delete the file. - file.appendChild(doc.createTextNode(op.file)); - removeFiles.appendChild(file); - qDebug() << "Will remove file" << op.file; - } - break; - - default: - qWarning() << "Can't write update operation of type" << op.type - << "to file. Not implemented."; - continue; - } - } - - // Write the XML document to the file. - QFile outFile(scriptFile); - - if (outFile.open(QIODevice::WriteOnly)) - { - outFile.write(doc.toByteArray()); - } - else - { - return false; - } - - return true; -} - - } \ No newline at end of file diff --git a/logic/updater/GoUpdate.h b/logic/updater/GoUpdate.h index 941c4e3a..21976f8f 100644 --- a/logic/updater/GoUpdate.h +++ b/logic/updater/GoUpdate.h @@ -88,22 +88,17 @@ struct Operation OP_DELETE, } type; - //! The file to operate on. If this is a DELETE or CHMOD operation, this is the file that will be modified. + //! The file to operate on. QString file; - //! The destination file. If this is a DELETE or CHMOD operation, this field will be ignored. + //! The destination file. QString dest; - //! The mode to change the source file to. Ignored if this isn't a CHMOD operation. + //! The mode to change the source file to. int mode; }; typedef QList OperationList; -/** - * Takes the @OperationList list and writes an install script for the updater to the update files directory. - */ -bool writeInstallScript(OperationList& opsList, QString scriptFile); - /** * Loads the file list from the given version info JSON object into the given list. */ diff --git a/tests/tst_DownloadTask.cpp b/tests/tst_DownloadTask.cpp index 289aa195..eb58762b 100644 --- a/tests/tst_DownloadTask.cpp +++ b/tests/tst_DownloadTask.cpp @@ -69,20 +69,6 @@ slots: { } - void test_writeInstallScript() - { - OperationList ops; - - ops << Operation::CopyOp("sourceOne", "destOne", 0777) - << Operation::CopyOp("MultiMC.exe", "M/u/l/t/i/M/C/e/x/e") - << Operation::DeleteOp("toDelete.abc"); - auto testFile = "tests/data/tst_DownloadTask-test_writeInstallScript.xml"; - const QString script = QDir::temp().absoluteFilePath("MultiMCUpdateScript.xml"); - QVERIFY(writeInstallScript(ops, script)); - QCOMPARE(TestsInternal::readFileUtf8(script).replace(QRegExp("[\r\n]+"), "\n"), - MULTIMC_GET_TEST_FILE_UTF8(testFile).replace(QRegExp("[\r\n]+"), "\n")); - } - void test_parseVersionInfo_data() { QTest::addColumn("data");