diff options
| author | 2014-12-06 17:33:25 -0500 | |
|---|---|---|
| committer | 2014-12-06 17:33:25 -0500 | |
| commit | 7167ce41b61d2ba2cdb526777a4233eb84a3b66a (patch) | |
| tree | a35c14143716e1f2c98f808c81f89426045a946f /SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp | |
Imported Upstream version 2.99.6upstream/2.99.6
Diffstat (limited to 'SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp')
| -rw-r--r-- | SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp | 1058 |
1 files changed, 1058 insertions, 0 deletions
diff --git a/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp b/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp new file mode 100644 index 0000000..dae8238 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/updatemanager.cpp @@ -0,0 +1,1058 @@ +#include "updatemanager.h" +#include "services/pluginmanager.h" +#include "services/notifymanager.h" +#include "common/unused.h" +#include <QTemporaryDir> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QUrl> +#include <QUrlQuery> +#include <QDebug> +#include <QCoreApplication> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <QProcess> +#include <QThread> +#include <QtConcurrent/QtConcurrent> + +#ifdef Q_OS_WIN32 +#include "JlCompress.h" +#include <windows.h> +#include <shellapi.h> +#endif + +// Note on creating update packages: +// Packages for Linux and MacOSX should be an archive of _contents_ of SQLiteStudio directory, +// while for Windows it should be an archive of SQLiteStudio directory itself. + +QString UpdateManager::staticErrorMessage; +UpdateManager::RetryFunction UpdateManager::retryFunction = nullptr; + +UpdateManager::UpdateManager(QObject *parent) : + QObject(parent) +{ + networkManager = new QNetworkAccessManager(this); + connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*))); + connect(this, SIGNAL(updatingError(QString)), NOTIFY_MANAGER, SLOT(error(QString))); +} + +UpdateManager::~UpdateManager() +{ + cleanup(); +} + +void UpdateManager::checkForUpdates() +{ + getUpdatesMetadata(updatesCheckReply); +} + +void UpdateManager::update() +{ + if (updatesGetUrlsReply || updatesInProgress) + return; + + getUpdatesMetadata(updatesGetUrlsReply); +} + +QString UpdateManager::getPlatformForUpdate() const +{ +#if defined(Q_OS_LINUX) + if (QSysInfo::WordSize == 64) + return "linux64"; + else + return "linux32"; +#elif defined(Q_OS_WIN) + return "win32"; +#elif defined(Q_OS_OSX) + return "macosx"; +#else + return QString(); +#endif +} + +QString UpdateManager::getCurrentVersions() const +{ + QJsonArray versionsArray; + + QJsonObject arrayEntry; + arrayEntry["component"] = "SQLiteStudio"; + arrayEntry["version"] = SQLITESTUDIO->getVersionString(); + versionsArray.append(arrayEntry); + + for (const PluginManager::PluginDetails& details : PLUGINS->getAllPluginDetails()) + { + if (details.builtIn) + continue; + + arrayEntry["component"] = details.name; + arrayEntry["version"] = details.versionString; + versionsArray.append(arrayEntry); + } + + QJsonObject topObj; + topObj["versions"] = versionsArray; + + QJsonDocument doc(topObj); + return QString::fromLatin1(doc.toJson(QJsonDocument::Compact)); +} + +bool UpdateManager::isPlatformEligibleForUpdate() const +{ + return !getPlatformForUpdate().isNull() && getDistributionType() != DistributionType::OS_MANAGED; +} + +#if defined(Q_OS_WIN32) +bool UpdateManager::executePreFinalStepWin(const QString &tempDir, const QString &backupDir, const QString &appDir, bool reqAdmin) +{ + bool res; + if (reqAdmin) + res = executeFinalStepAsRootWin(tempDir, backupDir, appDir); + else + res = executeFinalStep(tempDir, backupDir, appDir); + + if (res) + { + QFileInfo path(qApp->applicationFilePath()); + QProcess::startDetached(appDir + "/" + path.fileName(), {WIN_POST_FINAL_UPDATE_OPTION_NAME, tempDir}); + } + return res; +} +#endif + +void UpdateManager::handleAvailableUpdatesReply(QNetworkReply* reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + updatingFailed(tr("An error occurred while checking for updates: %1.").arg(reply->errorString())); + reply->deleteLater(); + return; + } + + QJsonParseError err; + QByteArray data = reply->readAll(); + reply->deleteLater(); + + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) + { + qWarning() << "Invalid response from update service:" << err.errorString() << "\n" << "The data was:" << QString::fromLatin1(data); + notifyWarn(tr("Could not check available updates, because server responded with invalid message format. It is safe to ignore this warning.")); + return; + } + + QList<UpdateEntry> updates = readMetadata(doc); + if (updates.size() > 0) + emit updatesAvailable(updates); + else + emit noUpdatesAvailable(); +} + +void UpdateManager::getUpdatesMetadata(QNetworkReply*& replyStoragePointer) +{ +#ifndef NO_AUTO_UPDATES + if (!isPlatformEligibleForUpdate() || replyStoragePointer) + return; + + QUrlQuery query; + query.addQueryItem("platform", getPlatformForUpdate()); + query.addQueryItem("data", getCurrentVersions()); + + QUrl url(QString::fromLatin1(updateServiceUrl) + "?" + query.query(QUrl::FullyEncoded)); + QNetworkRequest request(url); + replyStoragePointer = networkManager->get(request); +#endif +} + +void UpdateManager::handleUpdatesMetadata(QNetworkReply* reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + updatingFailed(tr("An error occurred while reading updates metadata: %1.").arg(reply->errorString())); + reply->deleteLater(); + return; + } + + QJsonParseError err; + QByteArray data = reply->readAll(); + reply->deleteLater(); + + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) + { + qWarning() << "Invalid response from update service for getting metadata:" << err.errorString() << "\n" << "The data was:" << QString::fromLatin1(data); + notifyWarn(tr("Could not download updates, because server responded with invalid message format. " + "You can try again later or download and install updates manually. See <a href=\"%1\">User Manual</a> for details.").arg(manualUpdatesHelpUrl)); + return; + } + + tempDir = new QTemporaryDir(); + if (!tempDir->isValid()) { + notifyWarn(tr("Could not create temporary directory for downloading the update. Updating aborted.")); + return; + } + + updatesInProgress = true; + updatesToDownload = readMetadata(doc); + totalDownloadsCount = updatesToDownload.size(); + totalPercent = 0; + + if (totalDownloadsCount == 0) + { + updatingFailed(tr("There was no updates to download. Updating aborted.")); + return; + } + + downloadUpdates(); +} + +QList<UpdateManager::UpdateEntry> UpdateManager::readMetadata(const QJsonDocument& doc) +{ + QList<UpdateEntry> updates; + UpdateEntry entry; + QJsonObject obj = doc.object(); + QJsonArray versionsArray = obj["newVersions"].toArray(); + QJsonObject entryObj; + for (const QJsonValue& value : versionsArray) + { + entryObj = value.toObject(); + entry.compontent = entryObj["component"].toString(); + entry.version = entryObj["version"].toString(); + entry.url = entryObj["url"].toString(); + updates << entry; + } + + return updates; +} + +void UpdateManager::downloadUpdates() +{ + if (updatesToDownload.size() == 0) + { + QtConcurrent::run(this, &UpdateManager::installUpdates); + return; + } + + UpdateEntry entry = updatesToDownload.takeFirst(); + currentJobTitle = tr("Downloading: %1").arg(entry.compontent); + emit updatingProgress(currentJobTitle, 0, totalPercent); + + QStringList parts = entry.url.split("/"); + if (parts.size() < 1) + { + updatingFailed(tr("Could not determinate file name from update URL: %1. Updating aborted.").arg(entry.url)); + return; + } + + QString path = tempDir->path() + QLatin1Char('/') + parts.last(); + currentDownloadFile = new QFile(path); + if (!currentDownloadFile->open(QIODevice::WriteOnly)) + { + updatingFailed(tr("Failed to open file '%1' for writting: %2. Updating aborted.").arg(path, currentDownloadFile->errorString())); + return; + } + + updatesToInstall[entry.compontent] = path; + + QNetworkRequest request(QUrl(entry.url)); + updatesGetReply = networkManager->get(request); + connect(updatesGetReply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgress(qint64,qint64))); + connect(updatesGetReply, SIGNAL(readyRead()), this, SLOT(readDownload())); +} + +void UpdateManager::updatingFailed(const QString& errMsg) +{ + cleanup(); + updatesInProgress = false; + emit updatingError(errMsg); +} + +void UpdateManager::installUpdates() +{ + currentJobTitle = tr("Installing updates."); + totalPercent = (totalDownloadsCount - updatesToDownload.size()) * 100 / (totalDownloadsCount + 1); + emit updatingProgress(currentJobTitle, 0, totalPercent); + + requireAdmin = doRequireAdminPrivileges(); + + QTemporaryDir installTempDir; + QString appDirName = QDir(getAppDirPath()).dirName(); + QString targetDir = installTempDir.path() + QLatin1Char('/') + appDirName; + if (!copyRecursively(getAppDirPath(), targetDir)) + { + updatingFailed(tr("Could not copy current application directory into %1 directory.").arg(installTempDir.path())); + return; + } + emit updatingProgress(currentJobTitle, 40, totalPercent); + + int i = 0; + int updatesCnt = updatesToInstall.size(); + for (const QString& component : updatesToInstall.keys()) + { + if (!installComponent(component, targetDir)) + { + cleanup(); + updatesInProgress = false; + return; + } + i++; + emit updatingProgress(currentJobTitle, (30 + (50 / updatesCnt * i)), totalPercent); + } + + if (!executeFinalStep(targetDir)) + { + cleanup(); + updatesInProgress = false; + return; + } + + currentJobTitle = QString(); + totalPercent = 100; + emit updatingProgress(currentJobTitle, 100, totalPercent); + cleanup(); + updatesInProgress = false; +#ifdef Q_OS_WIN32 + installTempDir.setAutoRemove(false); +#endif + + SQLITESTUDIO->setImmediateQuit(true); + qApp->exit(0); +} + +bool UpdateManager::executeFinalStep(const QString& tempDir, const QString& backupDir, const QString& appDir) +{ + bool isWin = false; +#ifdef Q_OS_WIN32 + isWin = true; + + // Windows needs to wait for previus process to exit + QThread::sleep(3); + + QDir dir(backupDir); + QString dirName = dir.dirName(); + dir.cdUp(); + if (!dir.mkdir(dirName)) + { + staticUpdatingFailed(tr("Could not create directory %1.").arg(backupDir)); + return false; + } +#endif + while (!moveDir(appDir, backupDir, isWin)) + { + if (!retryFunction) + { + staticUpdatingFailed(tr("Could not rename directory %1 to %2.\nDetails: %3").arg(appDir, backupDir, staticErrorMessage)); + return false; + } + + if (!retryFunction(tr("Cannot not rename directory %1 to %2.\nDetails: %3").arg(appDir, backupDir, staticErrorMessage))) + return false; + } + + if (!moveDir(tempDir, appDir, isWin)) + { + if (!moveDir(backupDir, appDir, isWin)) + { + staticUpdatingFailed(tr("Could not move directory %1 to %2 and also failed to restore original directory, " + "so the original SQLiteStudio directory is now located at: %3").arg(tempDir, appDir, backupDir)); + } + else + { + staticUpdatingFailed(tr("Could not rename directory %1 to %2. Rolled back to the original SQLiteStudio version.").arg(tempDir, appDir)); + } + deleteDir(backupDir); + return false; + } + + deleteDir(backupDir); + return true; +} + +bool UpdateManager::handleUpdateOptions(const QStringList& argList, int& returnCode) +{ + if (argList.size() == 5 && argList[1] == UPDATE_OPTION_NAME) + { + bool result = UpdateManager::executeFinalStep(argList[2], argList[3], argList[4]); + if (result) + returnCode = 0; + else + returnCode = 1; + + return true; + } + +#ifdef Q_OS_WIN32 + if (argList.size() == 6 && argList[1] == WIN_PRE_FINAL_UPDATE_OPTION_NAME) + { + bool result = UpdateManager::executePreFinalStepWin(argList[2], argList[3], argList[4], (bool)argList[5].toInt()); + if (result) + returnCode = 0; + else + returnCode = -1; + + return true; + } + + if (argList.size() == 3 && argList[1] == WIN_POST_FINAL_UPDATE_OPTION_NAME) + { + QThread::sleep(1); // to make sure that the previous process has quit + returnCode = 0; + UpdateManager::executePostFinalStepWin(argList[2]); + return true; + } +#endif + + return false; +} + +QString UpdateManager::getStaticErrorMessage() +{ + return staticErrorMessage; +} + +bool UpdateManager::executeFinalStep(const QString& tempDir) +{ + QString appDir = getAppDirPath(); + + // Find inexisting dir name next to app dir + QDir backupDir(getBackupDir(appDir)); + +#if defined(Q_OS_WIN32) + return runAnotherInstanceForUpdate(tempDir, backupDir.absolutePath(), qApp->applicationDirPath(), requireAdmin); +#else + bool res; + if (requireAdmin) + res = executeFinalStepAsRoot(tempDir, backupDir.absolutePath(), appDir); + else + res = executeFinalStep(tempDir, backupDir.absolutePath(), appDir); + + if (res) + QProcess::startDetached(qApp->applicationFilePath(), QStringList()); + + return res; +#endif +} + +bool UpdateManager::installComponent(const QString& component, const QString& tempDir) +{ + if (!unpackToDir(updatesToInstall[component], tempDir)) + { + updatingFailed(tr("Could not unpack component %1 into %2 directory.").arg(component, tempDir)); + return false; + } + + // In future here we might also delete/change some files, according to some update script. + return true; +} + +void UpdateManager::cleanup() +{ + safe_delete(currentDownloadFile); + safe_delete(tempDir); + updatesToDownload.clear(); + updatesToInstall.clear(); + requireAdmin = false; +} + +bool UpdateManager::waitForProcess(QProcess& proc) +{ + if (!proc.waitForFinished(-1)) + { + qDebug() << "Update QProcess timed out."; + return false; + } + + if (proc.exitStatus() == QProcess::CrashExit) + { + qDebug() << "Update QProcess finished by crashing."; + return false; + } + + if (proc.exitCode() != 0) + { + qDebug() << "Update QProcess finished with code:" << proc.exitCode(); + return false; + } + + return true; +} + +QString UpdateManager::readError(QProcess& proc, bool reverseOrder) +{ + QString err = QString::fromLocal8Bit(reverseOrder ? proc.readAllStandardOutput() : proc.readAllStandardError()); + if (err.isEmpty()) + err = QString::fromLocal8Bit(reverseOrder ? proc.readAllStandardError() : proc.readAllStandardOutput()); + + QString errStr = proc.errorString(); + if (!errStr.isEmpty()) + err += "\n" + errStr; + + return err; +} + +void UpdateManager::staticUpdatingFailed(const QString& errMsg) +{ +#if defined(Q_OS_WIN32) + staticErrorMessage = errMsg; +#else + UPDATES->handleStaticFail(errMsg); +#endif + qCritical() << errMsg; +} + +bool UpdateManager::executeFinalStepAsRoot(const QString& tempDir, const QString& backupDir, const QString& appDir) +{ +#if defined(Q_OS_LINUX) + return executeFinalStepAsRootLinux(tempDir, backupDir, appDir); +#elif defined(Q_OS_WIN32) + return executeFinalStepAsRootWin(tempDir, backupDir, appDir); +#elif defined(Q_OS_MACX) + return executeFinalStepAsRootMac(tempDir, backupDir, appDir); +#else + qCritical() << "Unknown update platform in UpdateManager::executeFinalStepAsRoot() for package" << packagePath; + return false; +#endif +} + +#if defined(Q_OS_LINUX) +bool UpdateManager::executeFinalStepAsRootLinux(const QString& tempDir, const QString& backupDir, const QString& appDir) +{ + QStringList args = {qApp->applicationFilePath(), UPDATE_OPTION_NAME, tempDir, backupDir, appDir}; + + QProcess proc; + LinuxPermElevator elevator = findPermElevatorForLinux(); + switch (elevator) + { + case LinuxPermElevator::KDESU: + proc.setProgram("kdesu"); + args.prepend("-t"); + proc.setArguments(args); + break; + case LinuxPermElevator::GKSU: + proc.setProgram("gksu"); // TODO test gksu updates + proc.setArguments(args); + break; + case LinuxPermElevator::PKEXEC: + { + // We call CLI for doing final step, because pkexec runs cmd completly in root env, so there's no X server. + args[0] += "cli"; + + QStringList newArgs; + for (const QString& arg : args) + newArgs << wrapCmdLineArgument(arg); + + QString cmd = "cd " + wrapCmdLineArgument(qApp->applicationDirPath()) +"; " + newArgs.join(" "); + + proc.setProgram("pkexec"); + proc.setArguments({"sh", "-c", cmd}); + } + break; + case LinuxPermElevator::NONE: + updatingFailed(tr("Could not find permissions elevator application to run update as a root. Looked for: %1").arg("kdesu, gksu, pkexec")); + return false; + } + + proc.start(); + if (!waitForProcess(proc)) + { + updatingFailed(tr("Could not execute final updating steps as root: %1").arg(readError(proc, (elevator == LinuxPermElevator::KDESU)))); + return false; + } + + return true; +} +#endif + +#ifdef Q_OS_MACX +bool UpdateManager::executeFinalStepAsRootMac(const QString& tempDir, const QString& backupDir, const QString& appDir) +{ + // Prepare script for updater + // osascript -e "do shell script \"stufftorunasroot\" with administrator privileges" + QStringList args = {wrapCmdLineArgument(qApp->applicationFilePath() + "cli"), + UPDATE_OPTION_NAME, + wrapCmdLineArgument(tempDir), + wrapCmdLineArgument(backupDir), + wrapCmdLineArgument(appDir)}; + QProcess proc; + + QString innerCmd = wrapCmdLineArgument(args.join(" ")); + + static_qstring(scriptTpl, "do shell script %1 with administrator privileges"); + QString scriptCmd = scriptTpl.arg(innerCmd); + + // Prepare updater temporary directory + QTemporaryDir updaterDir; + if (!updaterDir.isValid()) + { + updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(tr("Cannot create temporary directory for updater."))); + return false; + } + + // Create updater script + QString scriptPath = updaterDir.path() + "/UpdateSQLiteStudio.scpt"; + QFile updaterScript(scriptPath); + if (!updaterScript.open(QIODevice::WriteOnly)) + { + updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(tr("Cannot create updater script file."))); + return false; + } + updaterScript.write(scriptCmd.toLocal8Bit()); + updaterScript.close(); + + // Compile script to updater application + QString updaterApp = updaterDir.path() + "/UpdateSQLiteStudio.app"; + proc.setProgram("osacompile"); + proc.setArguments({"-o", updaterApp, scriptPath}); + proc.start(); + if (!waitForProcess(proc)) + { + updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(readError(proc))); + return false; + } + + // Execute updater + proc.setProgram(updaterApp + "/Contents/MacOS/applet"); + proc.setArguments({}); + proc.start(); + if (!waitForProcess(proc)) + { + updatingFailed(tr("Could not execute final updating steps as admin: %1").arg(readError(proc))); + return false; + } + + // Validating update + // The updater script will not return error if the user canceled the password prompt. + // We need to check if the update was actually made and return true only then. + if (QDir(tempDir).exists()) + { + // Temp dir still exists, so it was not moved by root process + updatingFailed(tr("Updating canceled.")); + return false; + } + + return true; +} +#endif + +#ifdef Q_OS_WIN32 +bool UpdateManager::executeFinalStepAsRootWin(const QString& tempDir, const QString& backupDir, const QString& appDir) +{ + QString updateBin = qApp->applicationDirPath() + "/" + WIN_UPDATER_BINARY; + + QString installFilePath = tempDir + "/" + WIN_INSTALL_FILE; + QFile installFile(installFilePath); + installFile.open(QIODevice::WriteOnly); + QString nl("\n"); + installFile.write(UPDATE_OPTION_NAME); + installFile.write(nl.toLocal8Bit()); + installFile.write(backupDir.toLocal8Bit()); + installFile.write(nl.toLocal8Bit()); + installFile.write(appDir.toLocal8Bit()); + installFile.write(nl.toLocal8Bit()); + installFile.close(); + + int res = (int)::ShellExecuteA(0, "runas", updateBin.toUtf8().constData(), 0, 0, SW_SHOWNORMAL); + if (res < 32) + { + staticUpdatingFailed(tr("Could not execute final updating steps as administrator.")); + return false; + } + + // Since I suck as a developer and I cannot implement a simple synchronous app call under Windows + // (QProcess does it somehow, but I'm too lazy to look it up and probably the solution wouldn't be compatible + // with our "privileges elevation" trick above... so after all I think we're stuck with this solution for now), + // I do the workaround here, which makes this process wait for the other process to create the "done" + // file when it's done, so this process knows when the other has ended. This way we can proceed with this + // process and we will delete some directories later on, which were required by that other process. + if (!waitForFileToDisappear(installFilePath, 10)) + { + staticUpdatingFailed(tr("Could not execute final updating steps as administrator. Updater startup timed out.")); + return false; + } + + if (!waitForFileToAppear(appDir + QLatin1Char('/') + WIN_UPDATE_DONE_FILE, 30)) + { + staticUpdatingFailed(tr("Could not execute final updating steps as administrator. Updater operation timed out.")); + return false; + } + + return true; +} +#endif + +#if defined(Q_OS_WIN32) +bool UpdateManager::executePostFinalStepWin(const QString &tempDir) +{ + QString doneFile = qApp->applicationDirPath() + QLatin1Char('/') + WIN_UPDATE_DONE_FILE; + QFile::remove(doneFile); + + QDir dir(tempDir); + dir.cdUp(); + if (!deleteDir(dir.absolutePath())) + staticUpdatingFailed(tr("Could not clean up temporary directory %1. You can delete it manually at any time.").arg(dir.absolutePath())); + + QProcess::startDetached(qApp->applicationFilePath(), QStringList()); + return true; +} + +bool UpdateManager::waitForFileToDisappear(const QString &filePath, int seconds) +{ + QFile file(filePath); + while (file.exists() && seconds > 0) + { + QThread::sleep(1); + seconds--; + } + + return !file.exists(); +} + +bool UpdateManager::waitForFileToAppear(const QString &filePath, int seconds) +{ + QFile file(filePath); + while (!file.exists() && seconds > 0) + { + QThread::sleep(1); + seconds--; + } + + return file.exists(); +} + +bool UpdateManager::runAnotherInstanceForUpdate(const QString &tempDir, const QString &backupDir, const QString &appDir, bool reqAdmin) +{ + bool res = QProcess::startDetached(tempDir + "/SQLiteStudio.exe", {WIN_PRE_FINAL_UPDATE_OPTION_NAME, tempDir, backupDir, appDir, + QString::number((int)reqAdmin)}); + if (!res) + { + updatingFailed(tr("Could not run new version for continuing update.")); + return false; + } + + return true; +} +#endif + +UpdateManager::LinuxPermElevator UpdateManager::findPermElevatorForLinux() +{ +#if defined(Q_OS_LINUX) + QProcess proc; + proc.setProgram("which"); + + if (!SQLITESTUDIO->getEnv("DISPLAY").isEmpty()) + { + proc.setArguments({"kdesu"}); + proc.start(); + if (waitForProcess(proc)) + return LinuxPermElevator::KDESU; + + proc.setArguments({"gksu"}); + proc.start(); + if (waitForProcess(proc)) + return LinuxPermElevator::GKSU; + } + + proc.setArguments({"pkexec"}); + proc.start(); + if (waitForProcess(proc)) + return LinuxPermElevator::PKEXEC; +#endif + + return LinuxPermElevator::NONE; +} + +QString UpdateManager::wrapCmdLineArgument(const QString& arg) +{ + return "\"" + escapeCmdLineArgument(arg) + "\""; +} + +QString UpdateManager::escapeCmdLineArgument(const QString& arg) +{ + if (!arg.contains("\\") && !arg.contains("\"")) + return arg; + + QString str = arg; + return str.replace("\\", "\\\\").replace("\"", "\\\""); +} + +QString UpdateManager::getBackupDir(const QString &appDir) +{ + static_qstring(bakDirTpl, "%1.old%2"); + QDir backupDir(bakDirTpl.arg(appDir, "")); + int cnt = 1; + while (backupDir.exists()) + backupDir = QDir(bakDirTpl.arg(appDir, QString::number(cnt))); + + return backupDir.absolutePath(); +} + +bool UpdateManager::unpackToDir(const QString& packagePath, const QString& outputDir) +{ +#if defined(Q_OS_LINUX) + return unpackToDirLinux(packagePath, outputDir); +#elif defined(Q_OS_WIN32) + return unpackToDirWin(packagePath, outputDir); +#elif defined(Q_OS_MACX) + return unpackToDirMac(packagePath, outputDir); +#else + qCritical() << "Unknown update platform in UpdateManager::unpackToDir() for package" << packagePath; + return false; +#endif +} + +#if defined(Q_OS_LINUX) +bool UpdateManager::unpackToDirLinux(const QString &packagePath, const QString &outputDir) +{ + QProcess proc; + proc.setWorkingDirectory(outputDir); + proc.setStandardOutputFile(QProcess::nullDevice()); + proc.setStandardErrorFile(QProcess::nullDevice()); + + if (!packagePath.endsWith("tar.gz")) + { + updatingFailed(tr("Package not in tar.gz format, cannot install: %1").arg(packagePath)); + return false; + } + + proc.start("mv", {packagePath, outputDir}); + if (!waitForProcess(proc)) + { + updatingFailed(tr("Package %1 cannot be installed, because cannot move it to directory: %2").arg(packagePath, outputDir)); + return false; + } + + QString fileName = packagePath.split("/").last(); + QString newPath = outputDir + "/" + fileName; + proc.start("tar", {"-xzf", newPath}); + if (!waitForProcess(proc)) + { + updatingFailed(tr("Package %1 cannot be installed, because cannot unpack it: %2").arg(packagePath, readError(proc))); + return false; + } + + QProcess::execute("rm", {"-f", newPath}); + return true; +} +#endif + +#if defined(Q_OS_MACX) +bool UpdateManager::unpackToDirMac(const QString &packagePath, const QString &outputDir) +{ + QProcess proc; + proc.setWorkingDirectory(outputDir); + proc.setStandardOutputFile(QProcess::nullDevice()); + proc.setStandardErrorFile(QProcess::nullDevice()); + + if (!packagePath.endsWith("zip")) + { + updatingFailed(tr("Package not in zip format, cannot install: %1").arg(packagePath)); + return false; + } + + proc.start("unzip", {"-o", "-d", outputDir, packagePath}); + if (!waitForProcess(proc)) + { + updatingFailed(tr("Package %1 cannot be installed, because cannot unzip it to directory %2: %3") + .arg(packagePath, outputDir, readError(proc))); + return false; + } + + return true; +} +#endif + +#if defined(Q_OS_WIN32) +bool UpdateManager::unpackToDirWin(const QString& packagePath, const QString& outputDir) +{ + if (JlCompress::extractDir(packagePath, outputDir + "/..").isEmpty()) + { + updatingFailed(tr("Package %1 cannot be installed, because cannot unzip it to directory: %2").arg(packagePath, outputDir)); + return false; + } + + return true; +} +#endif + +void UpdateManager::handleStaticFail(const QString& errMsg) +{ + emit updatingFailed(errMsg); +} + +QString UpdateManager::getAppDirPath() const +{ + static QString appDir; + if (appDir.isNull()) + { + appDir = qApp->applicationDirPath(); +#ifdef Q_OS_MACX + QDir tmpAppDir(appDir); + tmpAppDir.cdUp(); + tmpAppDir.cdUp(); + appDir = tmpAppDir.absolutePath(); +#endif + } + return appDir; +} + +bool UpdateManager::moveDir(const QString& src, const QString& dst, bool contentsOnly) +{ + // If we're doing a rename in the very same parent directory then we don't want + // the 'move between partitions' to be involved, cause any failure to rename + // is due to permissions or file lock. + QFileInfo srcFi(src); + QFileInfo dstFi(dst); + bool sameParentDir = (srcFi.dir() == dstFi.dir()); + + QDir dir; + if (contentsOnly) + { + QString localSrc; + QString localDst; + QDir srcDir(src); + for (const QFileInfo& entry : srcDir.entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::Hidden|QDir::System)) + { + localSrc = entry.absoluteFilePath(); + localDst = dst + "/" + entry.fileName(); + if (!dir.rename(localSrc, localDst) && (sameParentDir || !renameBetweenPartitions(localSrc, localDst))) + { + staticUpdatingFailed(tr("Could not rename directory %1 to %2.").arg(localSrc, localDst)); + return false; + } + } + } + else + { + if (!dir.rename(src, dst) && (sameParentDir || !renameBetweenPartitions(src, dst))) + { + staticUpdatingFailed(tr("Could not rename directory %1 to %2.").arg(src, dst)); + return false; + } + } + + return true; +} + +bool UpdateManager::deleteDir(const QString& path) +{ + QDir dir(path); + if (!dir.removeRecursively()) + { + staticUpdatingFailed(tr("Could not delete directory %1.").arg(path)); + return false; + } + + return true; +} + +bool UpdateManager::execCmd(const QString& cmd, const QStringList& args, QString* errorMsg) +{ + QProcess proc; + proc.start(cmd, args); + QString cmdString = QString("%1 \"%2\"").arg(cmd, args.join("\\\" \\\"")); + + if (!waitForProcess(proc)) + { + if (errorMsg) + *errorMsg = tr("Error executing update command: %1\nError message: %2").arg(cmdString).arg(readError(proc)); + + return false; + } + + return true; +} + +void UpdateManager::setRetryFunction(const RetryFunction &value) +{ + retryFunction = value; +} + +bool UpdateManager::doRequireAdminPrivileges() +{ + QString appDirPath = getAppDirPath(); + QDir appDir(appDirPath); + bool isWritable = isWritableRecursively(appDir.absolutePath()); + + appDir.cdUp(); + QFileInfo fi(appDir.absolutePath()); + isWritable &= fi.isWritable(); + + if (isWritable) + { + QDir backupDir(getBackupDir(appDirPath)); + QString backupDirName = backupDir.dirName(); + backupDir.cdUp(); + if (backupDir.mkdir(backupDirName)) + backupDir.rmdir(backupDirName); + else + isWritable = false; + } + + return !isWritable; +} + +void UpdateManager::finished(QNetworkReply* reply) +{ + if (reply == updatesCheckReply) + { + updatesCheckReply = nullptr; + handleAvailableUpdatesReply(reply); + return; + } + + if (reply == updatesGetUrlsReply) + { + updatesGetUrlsReply = nullptr; + handleUpdatesMetadata(reply); + return; + } + + if (reply == updatesGetReply) + { + handleDownloadReply(reply); + if (reply == updatesGetReply) // if no new download is requested + updatesGetReply = nullptr; + + return; + } +} + +void UpdateManager::handleDownloadReply(QNetworkReply* reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + updatingFailed(tr("An error occurred while downloading updates: %1. Updating aborted.").arg(reply->errorString())); + reply->deleteLater(); + return; + } + + totalPercent = (totalDownloadsCount - updatesToDownload.size()) * 100 / (totalDownloadsCount + 1); + + readDownload(); + currentDownloadFile->close(); + + safe_delete(currentDownloadFile); + + reply->deleteLater(); + downloadUpdates(); +} + +void UpdateManager::downloadProgress(qint64 bytesReceived, qint64 totalBytes) +{ + int perc; + if (totalBytes < 0) + perc = -1; + else if (totalBytes == 0) + perc = 100; + else + perc = bytesReceived * 100 / totalBytes; + + emit updatingProgress(currentJobTitle, perc, totalPercent); +} + +void UpdateManager::readDownload() +{ + currentDownloadFile->write(updatesGetReply->readAll()); +} |
