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 (updateDlg.exec(&updateTask)) | ||||
| 	{ | ||||
| 		MMC->installUpdates(updateTask.updateFilesDir()); | ||||
| 		MMC->installUpdates(updateTask.updateFilesDir(), updateTask.operations()); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
|   | ||||
| @@ -583,7 +583,69 @@ std::shared_ptr<JavaVersionList> MultiMC::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."; | ||||
| #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 <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) | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| #include <QFlag> | ||||
| #include <QIcon> | ||||
| #include <QDateTime> | ||||
| #include <updater/GoUpdate.h> | ||||
|  | ||||
| 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 | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
| } | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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<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. | ||||
|  */ | ||||
|   | ||||
| @@ -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<QByteArray>("data"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Petr Mrázek
					Petr Mrázek