GH-1060 implement very basic updater (only linux and maybe osx right now)
This commit is contained in:
parent
166813cb91
commit
82e05661d2
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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.
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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");
|
||||||
|
Loading…
Reference in New Issue
Block a user