GH-1060 implement very basic updater (only linux and maybe osx right now)
This commit is contained in:
		| @@ -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"); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Petr Mrázek
					Petr Mrázek