GH-1060 implement very basic updater (only linux and maybe osx right now)

This commit is contained in:
Petr Mrázek 2015-06-08 02:43:16 +02:00
parent 166813cb91
commit 82e05661d2
8 changed files with 170 additions and 116 deletions

View File

@ -992,7 +992,7 @@ void MainWindow::downloadUpdates(GoUpdate::Status status)
// If the task succeeds, install the updates. // If the task succeeds, install the updates.
if (updateDlg.exec(&updateTask)) if (updateDlg.exec(&updateTask))
{ {
MMC->installUpdates(updateTask.updateFilesDir()); MMC->installUpdates(updateTask.updateFilesDir(), updateTask.operations());
} }
else else
{ {

View File

@ -583,7 +583,69 @@ std::shared_ptr<JavaVersionList> MultiMC::javalist()
return m_javalist; return m_javalist;
} }
void MultiMC::installUpdates(const QString updateFilesDir) // from <sys/stat.h>
#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."; qDebug() << "Installing updates.";
#ifdef WINDOWS #ifdef WINDOWS
@ -596,14 +658,96 @@ void MultiMC::installUpdates(const QString updateFilesDir)
#error Unsupported operating system. #error Unsupported operating system.
#endif #endif
QStringList args; QString backupPath = PathCombine(root(), "update-backup");
args << "--install-dir" << root(); QString trashPath = PathCombine(root(), "update-trash");
args << "--package-dir" << updateFilesDir; if(!ensureFolderPathExists(backupPath))
args << "--script" << PathCombine(updateFilesDir, "file_list.xml"); {
args << "--wait" << QString::number(applicationPid()); qWarning() << "couldn't create folder" << backupPath;
args << "--finish-cmd" << finishCmd; return;
args << "--finish-dir" << dataPath; }
qDebug() << "Running updater with args" << args.join(" "); if(!ensureFolderPathExists(trashPath))
{
qWarning() << "couldn't create folder" << trashPath;
return;
}
struct BackupEntry
{
QString orig;
QString backup;
};
QList <BackupEntry> backups;
QList <BackupEntry> 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) void MultiMC::setIconTheme(const QString& name)

View File

@ -6,6 +6,7 @@
#include <QFlag> #include <QFlag>
#include <QIcon> #include <QIcon>
#include <QDateTime> #include <QDateTime>
#include <updater/GoUpdate.h>
class QFile; class QFile;
class MinecraftVersionList; class MinecraftVersionList;
@ -105,7 +106,7 @@ public:
} }
// APPLICATION ONLY // 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 * Opens a json file using either a system default editor, or, if note empty, the editor

View File

@ -89,7 +89,6 @@ void DownloadTask::processDownloadedVersionInfo()
{ {
VersionFileList m_currentVersionFileList; VersionFileList m_currentVersionFileList;
VersionFileList m_newVersionFileList; VersionFileList m_newVersionFileList;
OperationList operationList;
setStatus(tr("Reading file list for new version...")); setStatus(tr("Reading file list for new version..."));
qDebug() << "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")); NetJobPtr netJob (new NetJob("Update Files"));
// fill netJob and operationList // 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...")); emitFailed(tr("Failed to process update lists..."));
return; 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. // Now start the download.
QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished); QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished);
QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged); QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged);
@ -170,4 +162,9 @@ QString DownloadTask::updateFilesDir()
return m_updateFilesDir.path(); return m_updateFilesDir.path();
} }
OperationList DownloadTask::operations()
{
return m_operations;
}
} }

View File

@ -35,6 +35,9 @@ public:
/// Get the directory that will contain the update files. /// Get the directory that will contain the update files.
QString updateFilesDir(); QString updateFilesDir();
/// Get the list of operations that should be done
OperationList operations();
/// set updater download behavior /// set updater download behavior
void setUseLocalUpdater(bool useLocal); void setUseLocalUpdater(bool useLocal);
@ -61,6 +64,8 @@ protected:
Status m_status; Status m_status;
OperationList m_operations;
/*! /*!
* Temporary directory to store update files in. * Temporary directory to store update files in.
* This will be set to not auto delete. Task will fail if this fails to be created. * This will be set to not auto delete. Task will fail if this fails to be created.

View File

@ -213,78 +213,4 @@ bool fixPathForOSX(QString &path)
return false; 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;
}
} }

View File

@ -88,22 +88,17 @@ struct Operation
OP_DELETE, OP_DELETE,
} type; } 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; QString file;
//! The destination file. If this is a DELETE or CHMOD operation, this field will be ignored. //! The destination file.
QString dest; 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; int mode;
}; };
typedef QList<Operation> OperationList; typedef QList<Operation> 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. * Loads the file list from the given version info JSON object into the given list.
*/ */

View File

@ -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() void test_parseVersionInfo_data()
{ {
QTest::addColumn<QByteArray>("data"); QTest::addColumn<QByteArray>("data");