diff options
Diffstat (limited to 'src/libs/installer/packagemanagercore_p.cpp')
-rw-r--r-- | src/libs/installer/packagemanagercore_p.cpp | 2326 |
1 files changed, 2326 insertions, 0 deletions
diff --git a/src/libs/installer/packagemanagercore_p.cpp b/src/libs/installer/packagemanagercore_p.cpp new file mode 100644 index 000000000..765db4e4e --- /dev/null +++ b/src/libs/installer/packagemanagercore_p.cpp @@ -0,0 +1,2326 @@ +/************************************************************************** +** +** This file is part of Installer Framework +** +** Copyright (c) 2011-2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +**************************************************************************/ +#include "packagemanagercore_p.h" + +#include "adminauthorization.h" +#include "binaryformat.h" +#include "component.h" +#include "errors.h" +#include "fileutils.h" +#include "fsengineclient.h" +#include "messageboxhandler.h" +#include "packagemanagercore.h" +#include "progresscoordinator.h" +#include "qprocesswrapper.h" +#include "qsettingswrapper.h" + +#include "kdsavefile.h" +#include "kdselfrestarter.h" +#include "kdupdaterfiledownloaderfactory.h" +#include "kdupdaterupdatesourcesinfo.h" +#include "kdupdaterupdateoperationfactory.h" +#include "kdupdaterupdatefinder.h" + +#include <QtCore/QtConcurrentRun> +#include <QtCore/QCoreApplication> +#include <QtCore/QDir> +#include <QtCore/QDirIterator> +#include <QtCore/QFuture> +#include <QtCore/QFutureWatcher> +#include <QtCore/QTemporaryFile> + +#include <QtXml/QXmlStreamReader> +#include <QtXml/QXmlStreamWriter> + +#include <errno.h> + +namespace QInstaller { + +static bool runOperation(Operation *op, PackageManagerCorePrivate::OperationType type) +{ + switch (type) { + case PackageManagerCorePrivate::Backup: + op->backup(); + return true; + case PackageManagerCorePrivate::Perform: + return op->performOperation(); + case PackageManagerCorePrivate::Undo: + return op->undoOperation(); + default: + Q_ASSERT(!"unexpected operation type"); + } + return false; +} + +/*! + \internal + Creates and initializes a FSEngineClientHandler -> makes us get admin rights for QFile operations +*/ +static FSEngineClientHandler *sClientHandlerInstance = 0; +static FSEngineClientHandler *initFSEngineClientHandler() +{ + if (sClientHandlerInstance == 0) { + sClientHandlerInstance = &FSEngineClientHandler::instance(); + + // Initialize the created FSEngineClientHandler instance. + const int port = 30000 + qrand() % 1000; + sClientHandlerInstance->init(port); + sClientHandlerInstance->setStartServerCommand(QCoreApplication::applicationFilePath(), + QStringList() << QLatin1String("--startserver") << QString::number(port) + << sClientHandlerInstance->authorizationKey(), true); + } + return sClientHandlerInstance; +} + +static QStringList checkRunningProcessesFromList(const QStringList &processList) +{ + const QList<ProcessInfo> allProcesses = runningProcesses(); + QStringList stillRunningProcesses; + foreach (const QString &process, processList) { + if (!process.isEmpty() && PackageManagerCorePrivate::isProcessRunning(process, allProcesses)) + stillRunningProcesses.append(process); + } + return stillRunningProcesses; +} + +static void deferredRename(const QString &oldName, const QString &newName, bool restart = false) +{ +#ifdef Q_OS_WIN + QStringList arguments; + { + QTemporaryFile f(QDir::temp().absoluteFilePath(QLatin1String("deferredrenameXXXXXX.vbs"))); + openForWrite(&f, f.fileName()); + f.setAutoRemove(false); + + arguments << QDir::toNativeSeparators(f.fileName()) << QDir::toNativeSeparators(oldName) + << QDir::toNativeSeparators(QFileInfo(oldName).dir().absoluteFilePath(QFileInfo(newName) + .fileName())); + + QTextStream batch(&f); + batch << "Set fso = WScript.CreateObject(\"Scripting.FileSystemObject\")\n"; + batch << "Set tmp = WScript.CreateObject(\"WScript.Shell\")\n"; + batch << QString::fromLatin1("file = \"%1\"\n").arg(arguments[2]); + batch << "on error resume next\n"; + + batch << "while fso.FileExists(file)\n"; + batch << " fso.DeleteFile(file)\n"; + batch << " WScript.Sleep(1000)\n"; + batch << "wend\n"; + batch << QString::fromLatin1("fso.MoveFile \"%1\", file\n").arg(arguments[1]); + if (restart) + batch << QString::fromLatin1("tmp.exec \"%1 --updater\"\n").arg(arguments[2]); + batch << "fso.DeleteFile(WScript.ScriptFullName)\n"; + } + + QProcessWrapper::startDetached(QLatin1String("cscript"), QStringList() << QLatin1String("//Nologo") + << arguments[0]); +#else + QFile::remove(newName); + QFile::rename(oldName, newName); + KDSelfRestarter::setRestartOnQuit(restart); +#endif +} + + +// -- PackageManagerCorePrivate + +PackageManagerCorePrivate::PackageManagerCorePrivate(PackageManagerCore *core) + : m_updateFinder(0) + , m_FSEngineClientHandler(0) + , m_core(core) + , m_repoMetaInfoJob(0) + , m_updates(false) + , m_repoFetched(false) + , m_updateSourcesAdded(false) + , m_componentsToInstallCalculated(false) + , m_proxyFactory(0) + , m_createLocalRepositoryFromBinary(false) +{ +} + +PackageManagerCorePrivate::PackageManagerCorePrivate(PackageManagerCore *core, qint64 magicInstallerMaker, + const OperationList &performedOperations) + : m_updateFinder(0) + , m_FSEngineClientHandler(initFSEngineClientHandler()) + , m_status(PackageManagerCore::Unfinished) + , m_forceRestart(false) + , m_testChecksum(false) + , m_launchedAsRoot(AdminAuthorization::hasAdminRights()) + , m_completeUninstall(false) + , m_needToWriteUninstaller(false) + , m_performedOperationsOld(performedOperations) + , m_core(core) + , m_repoMetaInfoJob(0) + , m_updates(false) + , m_repoFetched(false) + , m_updateSourcesAdded(false) + , m_magicBinaryMarker(magicInstallerMaker) + , m_componentsToInstallCalculated(false) + , m_proxyFactory(0) + , m_createLocalRepositoryFromBinary(false) +{ + connect(this, SIGNAL(installationStarted()), m_core, SIGNAL(installationStarted())); + connect(this, SIGNAL(installationFinished()), m_core, SIGNAL(installationFinished())); + connect(this, SIGNAL(uninstallationStarted()), m_core, SIGNAL(uninstallationStarted())); + connect(this, SIGNAL(uninstallationFinished()), m_core, SIGNAL(uninstallationFinished())); +} + +PackageManagerCorePrivate::~PackageManagerCorePrivate() +{ + clearAllComponentLists(); + clearUpdaterComponentLists(); + clearComponentsToInstall(); + + qDeleteAll(m_ownedOperations); + qDeleteAll(m_performedOperationsOld); + qDeleteAll(m_performedOperationsCurrentSession); + + // check for fake installer case + if (m_FSEngineClientHandler) + m_FSEngineClientHandler->setActive(false); + + delete m_updateFinder; + delete m_proxyFactory; +} + +/*! + Return true, if a process with \a name is running. On Windows, comparison is case-insensitive. +*/ +/* static */ +bool PackageManagerCorePrivate::isProcessRunning(const QString &name, + const QList<ProcessInfo> &processes) +{ + QList<ProcessInfo>::const_iterator it; + for (it = processes.constBegin(); it != processes.constEnd(); ++it) { + if (it->name.isEmpty()) + continue; + +#ifndef Q_WS_WIN + if (it->name == name) + return true; + const QFileInfo fi(it->name); + if (fi.fileName() == name || fi.baseName() == name) + return true; +#else + if (it->name.toLower() == name.toLower()) + return true; + if (it->name.toLower() == QDir::toNativeSeparators(name.toLower())) + return true; + const QFileInfo fi(it->name); + if (fi.fileName().toLower() == name.toLower() || fi.baseName().toLower() == name.toLower()) + return true; +#endif + } + return false; +} + +/* static */ +bool PackageManagerCorePrivate::performOperationThreaded(Operation *operation, OperationType type) +{ + QFutureWatcher<bool> futureWatcher; + const QFuture<bool> future = QtConcurrent::run(runOperation, operation, type); + + QEventLoop loop; + loop.connect(&futureWatcher, SIGNAL(finished()), SLOT(quit()), Qt::QueuedConnection); + futureWatcher.setFuture(future); + + if (!future.isFinished()) + loop.exec(); + + return future.result(); +} + +QString PackageManagerCorePrivate::targetDir() const +{ + return m_core->value(scTargetDir); +} + +QString PackageManagerCorePrivate::configurationFileName() const +{ + return m_core->value(scTargetConfigurationFile, QLatin1String("components.xml")); +} + +QString PackageManagerCorePrivate::componentsXmlPath() const +{ + return QDir::toNativeSeparators(QDir(QDir::cleanPath(targetDir())) + .absoluteFilePath(configurationFileName())); +} + +bool PackageManagerCorePrivate::buildComponentTree(QHash<QString, Component*> &components, bool loadScript) +{ + try { + // append all components to their respective parents + QHash<QString, Component*>::const_iterator it; + for (it = components.begin(); it != components.end(); ++it) { + if (statusCanceledOrFailed()) + return false; + + QString id = it.key(); + QInstaller::Component *component = it.value(); + while (!id.isEmpty() && component->parentComponent() == 0) { + id = id.section(QLatin1Char('.'), 0, -2); + if (components.contains(id)) + components[id]->appendComponent(component); + } + } + + // append all components w/o parent to the direct list + foreach (QInstaller::Component *component, components) { + if (statusCanceledOrFailed()) + return false; + + if (component->parentComponent() == 0) + m_core->appendRootComponent(component); + } + + // after everything is set up, load the scripts + foreach (QInstaller::Component *component, components) { + if (statusCanceledOrFailed()) + return false; + + if (loadScript) + component->loadComponentScript(); + + // set the checked state for all components without child (means without tristate) + if (component->isCheckable() && !component->isTristate()) { + if (component->isDefault() && isInstaller()) + component->setCheckState(Qt::Checked); + else if (component->isInstalled()) + component->setCheckState(Qt::Checked); + } + } + } catch (const Error &error) { + clearAllComponentLists(); + emit m_core->finishAllComponentsReset(); + setStatus(PackageManagerCore::Failure, error.message()); + + // TODO: make sure we remove all message boxes inside the library at some point. + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), QLatin1String("Error"), + tr("Error"), error.message()); + return false; + } + return true; +} + +void PackageManagerCorePrivate::clearAllComponentLists() +{ + qDeleteAll(m_rootComponents); + m_rootComponents.clear(); + + m_rootDependencyReplacements.clear(); + + const QList<QPair<Component*, Component*> > list = m_componentsToReplaceAllMode.values(); + for (int i = 0; i < list.count(); ++i) + delete list.at(i).second; + m_componentsToReplaceAllMode.clear(); + m_componentsToInstallCalculated = false; +} + +void PackageManagerCorePrivate::clearUpdaterComponentLists() +{ + QSet<Component*> usedComponents = + QSet<Component*>::fromList(m_updaterComponents + m_updaterComponentsDeps); + + const QList<QPair<Component*, Component*> > list = m_componentsToReplaceUpdaterMode.values(); + for (int i = 0; i < list.count(); ++i) { + if (usedComponents.contains(list.at(i).second)) + qWarning() << "a replacement was already in the list - is that correct?"; + else + usedComponents.insert(list.at(i).second); + } + + qDeleteAll(usedComponents); + + m_updaterComponents.clear(); + m_updaterComponentsDeps.clear(); + + m_updaterDependencyReplacements.clear(); + + m_componentsToReplaceUpdaterMode.clear(); + m_componentsToInstallCalculated = false; +} + +QList<Component *> &PackageManagerCorePrivate::replacementDependencyComponents(RunMode mode) +{ + return mode == AllMode ? m_rootDependencyReplacements : m_updaterDependencyReplacements; +} + +QHash<QString, QPair<Component*, Component*> > &PackageManagerCorePrivate::componentsToReplace(RunMode mode) +{ + return mode == AllMode ? m_componentsToReplaceAllMode : m_componentsToReplaceUpdaterMode; +} + +void PackageManagerCorePrivate::clearComponentsToInstall() +{ + m_visitedComponents.clear(); + m_toInstallComponentIds.clear(); + m_componentsToInstallError.clear(); + m_orderedComponentsToInstall.clear(); + m_toInstallComponentIdReasonHash.clear(); +} + +bool PackageManagerCorePrivate::appendComponentsToInstall(const QList<Component *> &components) +{ + if (components.isEmpty()) { + qDebug() << "components list is empty in" << Q_FUNC_INFO; + return true; + } + + QList<Component*> relevantComponentForAutoDependOn; + if (isUpdater()) + relevantComponentForAutoDependOn = m_updaterComponents + m_updaterComponentsDeps; + else { + foreach (QInstaller::Component *component, m_rootComponents) + relevantComponentForAutoDependOn += component->childComponents(true, AllMode); + } + + QList<Component*> notAppendedComponents; // for example components with unresolved dependencies + foreach (Component *component, components){ + if (m_toInstallComponentIds.contains(component->name())) { + QString errorMessage = QString::fromLatin1("Recursion detected component(%1) already added with " + "reason: \"%2\"").arg(component->name(), installReason(component)); + qDebug() << qPrintable(errorMessage); + m_componentsToInstallError.append(errorMessage); + Q_ASSERT_X(!m_toInstallComponentIds.contains(component->name()), Q_FUNC_INFO, + qPrintable(errorMessage)); + return false; + } + + if (component->dependencies().isEmpty()) + realAppendToInstallComponents(component); + else + notAppendedComponents.append(component); + } + + foreach (Component *component, notAppendedComponents) { + if (!appendComponentToInstall(component)) + return false; + } + + QList<Component *> foundAutoDependOnList; + // All regular dependencies are resolved. Now we are looking for auto depend on components. + foreach (Component *component, relevantComponentForAutoDependOn) { + // If a components is already installed or is scheduled for installation, no need to check for + // auto depend installation. + if ((!component->isInstalled() || component->updateRequested()) + && !m_toInstallComponentIds.contains(component->name())) { + // If we figure out a component requests auto installation, keep it to resolve their deps as + // well. + if (component->isAutoDependOn(m_toInstallComponentIds)) { + foundAutoDependOnList.append(component); + insertInstallReason(component, tr("Component(s) added as automatic dependencies")); + } + } + } + + if (!foundAutoDependOnList.isEmpty()) + return appendComponentsToInstall(foundAutoDependOnList); + return true; +} + +bool PackageManagerCorePrivate::appendComponentToInstall(Component *component) +{ + QSet<QString> allDependencies = component->dependencies().toSet(); + + foreach (const QString &dependencyComponentName, allDependencies) { + //componentByName return 0 if dependencyComponentName contains a version which is not available + Component *dependencyComponent = m_core->componentByName(dependencyComponentName); + if (dependencyComponent == 0) { + QString errorMessage; + if (!dependencyComponent) + errorMessage = QString::fromLatin1("Can't find missing dependency (%1) for %2."); + errorMessage = errorMessage.arg(dependencyComponentName, component->name()); + qDebug() << qPrintable(errorMessage); + m_componentsToInstallError.append(errorMessage); + Q_ASSERT_X(false, Q_FUNC_INFO, qPrintable(errorMessage)); + return false; + } + + if ((!dependencyComponent->isInstalled() || dependencyComponent->updateRequested()) + && !m_toInstallComponentIds.contains(dependencyComponent->name())) { + if (m_visitedComponents.value(component).contains(dependencyComponent)) { + QString errorMessage = QString::fromLatin1("Recursion detected component(%1) already " + "added with reason: \"%2\"").arg(component->name(), installReason(component)); + qDebug() << qPrintable(errorMessage); + m_componentsToInstallError = errorMessage; + Q_ASSERT_X(!m_visitedComponents.value(component).contains(dependencyComponent), Q_FUNC_INFO, + qPrintable(errorMessage)); + return false; + } + m_visitedComponents[component].insert(dependencyComponent); + + // add needed dependency components to the next run + insertInstallReason(dependencyComponent, tr("Added as dependency for %1.").arg(component->name())); + + if (!appendComponentToInstall(dependencyComponent)) + return false; + } + } + + if (!m_toInstallComponentIds.contains(component->name())) { + realAppendToInstallComponents(component); + insertInstallReason(component, tr("Component(s) that have resolved Dependencies")); + } + return true; +} + +QString PackageManagerCorePrivate::installReason(Component *component) +{ + const QString reason = m_toInstallComponentIdReasonHash.value(component->name()); + if (reason.isEmpty()) + return tr("Selected Component(s) without Dependencies"); + return m_toInstallComponentIdReasonHash.value(component->name()); +} + + +void PackageManagerCorePrivate::initialize() +{ + m_coreCheckedHash.clear(); + m_componentsToInstallCalculated = false; + m_createLocalRepositoryFromBinary = false; + + // first set some common variables that may used e.g. as placeholder + // in some of the settings variables or in a script or... + m_vars.insert(QLatin1String("rootDir"), QDir::rootPath()); + m_vars.insert(QLatin1String("homeDir"), QDir::homePath()); + m_vars.insert(scTargetConfigurationFile, QLatin1String("components.xml")); + +#ifdef Q_WS_WIN + m_vars.insert(QLatin1String("os"), QLatin1String("win")); +#elif defined(Q_WS_MAC) + m_vars.insert(QLatin1String("os"), QLatin1String("mac")); +#elif defined(Q_WS_X11) + m_vars.insert(QLatin1String("os"), QLatin1String("x11")); +#elif defined(Q_WS_QWS) + m_vars.insert(QLatin1String("os"), QLatin1String("Qtopia")); +#else + // TODO: add more platforms as needed... +#endif + + try { + m_settings = Settings(Settings::fromFileAndPrefix(QLatin1String(":/metadata/installer-config/config.xml"), + QLatin1String(":/metadata/installer-config/"))); + } catch (const Error &e) { + qCritical("Could not parse Config: %s", qPrintable(e.message())); + // TODO: try better error handling + return; + } + + // fill the variables defined in the settings + m_vars.insert(QLatin1String("ProductName"), m_settings.applicationName()); + m_vars.insert(QLatin1String("ProductVersion"), m_settings.applicationVersion()); + m_vars.insert(scTitle, m_settings.title()); + m_vars.insert(scPublisher, m_settings.publisher()); + m_vars.insert(QLatin1String("Url"), m_settings.url()); + m_vars.insert(scStartMenuDir, m_settings.startMenuDir()); + m_vars.insert(scTargetConfigurationFile, m_settings.configurationFileName()); + m_vars.insert(QLatin1String("LogoPixmap"), m_settings.logo()); + m_vars.insert(QLatin1String("LogoSmallPixmap"), m_settings.logoSmall()); + m_vars.insert(QLatin1String("WatermarkPixmap"), m_settings.watermark()); + + m_vars.insert(scRunProgram, replaceVariables(m_settings.runProgram())); + const QString desc = m_settings.runProgramDescription(); + if (!desc.isEmpty()) + m_vars.insert(scRunProgramDescription, desc); +#ifdef Q_WS_X11 + if (m_launchedAsRoot) + m_vars.insert(scTargetDir, replaceVariables(m_settings.adminTargetDir())); + else +#endif + m_vars.insert(scTargetDir, replaceVariables(m_settings.targetDir())); + m_vars.insert(scRemoveTargetDir, replaceVariables(m_settings.removeTargetDir())); + + QSettingsWrapper creatorSettings(QSettingsWrapper::IniFormat, QSettingsWrapper::UserScope, + QLatin1String("Nokia"), QLatin1String("QtCreator")); + QFileInfo info(creatorSettings.fileName()); + if (info.exists()) { + m_vars.insert(QLatin1String("QtCreatorSettingsFile"), info.absoluteFilePath()); + QDir settingsDirectory = info.absoluteDir(); + if (settingsDirectory.exists(QLatin1String("qtversion.xml"))) { + m_vars.insert(QLatin1String("QtCreatorSettingsQtVersionFile"), + settingsDirectory.absoluteFilePath(QLatin1String("qtversion.xml"))); + } + if (settingsDirectory.exists(QLatin1String("toolChains.xml"))) { + m_vars.insert(QLatin1String("QtCreatorSettingsToolchainsFile"), + settingsDirectory.absoluteFilePath(QLatin1String("toolChains.xml"))); + } + } + + if (!m_core->isInstaller()) { +#ifdef Q_WS_MAC + readMaintenanceConfigFiles(QCoreApplication::applicationDirPath() + QLatin1String("/../../..")); +#else + readMaintenanceConfigFiles(QCoreApplication::applicationDirPath()); +#endif + } + + foreach (Operation *currentOperation, m_performedOperationsOld) + currentOperation->setValue(QLatin1String("installer"), QVariant::fromValue(m_core)); + + disconnect(this, SIGNAL(installationStarted()), ProgressCoordinator::instance(), SLOT(reset())); + connect(this, SIGNAL(installationStarted()), ProgressCoordinator::instance(), SLOT(reset())); + disconnect(this, SIGNAL(uninstallationStarted()), ProgressCoordinator::instance(), SLOT(reset())); + connect(this, SIGNAL(uninstallationStarted()), ProgressCoordinator::instance(), SLOT(reset())); + + m_updaterApplication.updateSourcesInfo()->setFileName(QString()); + KDUpdater::PackagesInfo &packagesInfo = *m_updaterApplication.packagesInfo(); + packagesInfo.setFileName(componentsXmlPath()); + if (packagesInfo.applicationName().isEmpty()) + packagesInfo.setApplicationName(m_settings.applicationName()); + if (packagesInfo.applicationVersion().isEmpty()) + packagesInfo.setApplicationVersion(m_settings.applicationVersion()); + + if (isInstaller()) { + m_updaterApplication.addUpdateSource(m_settings.applicationName(), m_settings.applicationName(), + QString(), QUrl(QLatin1String("resource://metadata/")), 0); + m_updaterApplication.updateSourcesInfo()->setModified(false); + } + + if (!m_repoMetaInfoJob) { + m_repoMetaInfoJob = new GetRepositoriesMetaInfoJob(this); + m_repoMetaInfoJob->setAutoDelete(false); + connect(m_repoMetaInfoJob, SIGNAL(infoMessage(KDJob*, QString)), this, SLOT(infoMessage(KDJob*, + QString))); + } + KDUpdater::FileDownloaderFactory::instance().setProxyFactory(m_core->proxyFactory()); +} + +bool PackageManagerCorePrivate::isOfflineOnly() const +{ + if (!isInstaller()) + return false; + + QSettingsWrapper confInternal(QLatin1String(":/config/config-internal.ini"), QSettingsWrapper::IniFormat); + return confInternal.value(QLatin1String("offlineOnly"), false).toBool(); +} + +QString PackageManagerCorePrivate::installerBinaryPath() const +{ + return qApp->applicationFilePath(); +} + +bool PackageManagerCorePrivate::isInstaller() const +{ + return m_magicBinaryMarker == MagicInstallerMarker; +} + +bool PackageManagerCorePrivate::isUninstaller() const +{ + return m_magicBinaryMarker == MagicUninstallerMarker; +} + +bool PackageManagerCorePrivate::isUpdater() const +{ + return m_magicBinaryMarker == MagicUpdaterMarker; +} + +bool PackageManagerCorePrivate::isPackageManager() const +{ + return m_magicBinaryMarker == MagicPackageManagerMarker; +} + +bool PackageManagerCorePrivate::statusCanceledOrFailed() const +{ + return m_status == PackageManagerCore::Canceled || m_status == PackageManagerCore::Failure; +} + +void PackageManagerCorePrivate::setStatus(int status, const QString &error) +{ + m_error = error; + if (!error.isEmpty()) + qDebug() << m_error; + if (m_status != status) { + m_status = status; + emit m_core->statusChanged(PackageManagerCore::Status(m_status)); + } +} + +QString PackageManagerCorePrivate::replaceVariables(const QString &str) const +{ + static const QChar at = QLatin1Char('@'); + QString res; + int pos = 0; + while (true) { + const int pos1 = str.indexOf(at, pos); + if (pos1 == -1) + break; + const int pos2 = str.indexOf(at, pos1 + 1); + if (pos2 == -1) + break; + res += str.mid(pos, pos1 - pos); + const QString name = str.mid(pos1 + 1, pos2 - pos1 - 1); + res += m_core->value(name); + pos = pos2 + 1; + } + res += str.mid(pos); + return res; +} + +QByteArray PackageManagerCorePrivate::replaceVariables(const QByteArray &ba) const +{ + static const QChar at = QLatin1Char('@'); + QByteArray res; + int pos = 0; + while (true) { + const int pos1 = ba.indexOf(at, pos); + if (pos1 == -1) + break; + const int pos2 = ba.indexOf(at, pos1 + 1); + if (pos2 == -1) + break; + res += ba.mid(pos, pos1 - pos); + const QString name = QString::fromLocal8Bit(ba.mid(pos1 + 1, pos2 - pos1 - 1)); + res += m_core->value(name).toLocal8Bit(); + pos = pos2 + 1; + } + res += ba.mid(pos); + return res; +} + +/*! + \internal + Creates an update operation owned by the installer, not by any component. + */ +Operation *PackageManagerCorePrivate::createOwnedOperation(const QString &type) +{ + m_ownedOperations.append(KDUpdater::UpdateOperationFactory::instance().create(type)); + return m_ownedOperations.last(); +} + +/*! + \internal + Removes \a operation from the operations owned by the installer, returns the very same operation if the + operation was found, otherwise 0. + */ +Operation *PackageManagerCorePrivate::takeOwnedOperation(Operation *operation) +{ + if (!m_ownedOperations.contains(operation)) + return 0; + + m_ownedOperations.removeAll(operation); + return operation; +} + +QString PackageManagerCorePrivate::uninstallerName() const +{ + QString filename = m_settings.uninstallerName(); +#if defined(Q_WS_MAC) + if (QFileInfo(QCoreApplication::applicationDirPath() + QLatin1String("/../..")).isBundle()) + filename += QLatin1String(".app/Contents/MacOS/") + filename; +#elif defined(Q_OS_WIN) + filename += QLatin1String(".exe"); +#endif + return QString::fromLatin1("%1/%2").arg(targetDir()).arg(filename); +} + +static QNetworkProxy readProxy(QXmlStreamReader &reader) +{ + QNetworkProxy proxy(QNetworkProxy::HttpProxy); + while (reader.readNextStartElement()) { + if (reader.name() == QLatin1String("Host")) + proxy.setHostName(reader.readElementText()); + else if (reader.name() == QLatin1String("Port")) + proxy.setPort(reader.readElementText().toInt()); + else if (reader.name() == QLatin1String("Username")) + proxy.setUser(reader.readElementText()); + else if (reader.name() == QLatin1String("Password")) + proxy.setPassword(reader.readElementText()); + else + reader.skipCurrentElement(); + } + return proxy; +} + +static QSet<Repository> readRepositories(QXmlStreamReader &reader, bool isDefault) +{ + QSet<Repository> set; + while (reader.readNextStartElement()) { + if (reader.name() == QLatin1String("Repository")) { + Repository repo(QString(), isDefault); + while (reader.readNextStartElement()) { + if (reader.name() == QLatin1String("Host")) + repo.setUrl(reader.readElementText()); + else if (reader.name() == QLatin1String("Username")) + repo.setUsername(reader.readElementText()); + else if (reader.name() == QLatin1String("Password")) + repo.setPassword(reader.readElementText()); + else if (reader.name() == QLatin1String("Enabled")) + repo.setEnabled(bool(reader.readElementText().toInt())); + else + reader.skipCurrentElement(); + } + set.insert(repo); + } else { + reader.skipCurrentElement(); + } + } + return set; +} + +void PackageManagerCorePrivate::writeMaintenanceConfigFiles() +{ + // write current state (variables) to the uninstaller ini file + const QString iniPath = targetDir() + QLatin1Char('/') + m_settings.uninstallerIniFile(); + + QVariantHash vars; + QSettingsWrapper cfg(iniPath, QSettingsWrapper::IniFormat); + foreach (const QString &key, m_vars.keys()) { + if (key != scRunProgramDescription && key != scRunProgram) + vars.insert(key, m_vars.value(key)); + } + cfg.setValue(QLatin1String("Variables"), vars); + + QVariantList repos; + foreach (const Repository &repo, m_settings.defaultRepositories()) + repos.append(QVariant().fromValue(repo)); + cfg.setValue(QLatin1String("DefaultRepositories"), repos); + + cfg.sync(); + if (cfg.status() != QSettingsWrapper::NoError) { + const QString reason = cfg.status() == QSettingsWrapper::AccessError ? tr("Access error") + : tr("Format error"); + throw Error(tr("Could not write installer configuration to %1: %2").arg(iniPath, reason)); + } + + QFile file(targetDir() + QLatin1Char('/') + QLatin1String("network.xml")); + if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + QXmlStreamWriter writer(&file); + writer.setCodec("UTF-8"); + writer.setAutoFormatting(true); + writer.writeStartDocument(); + + writer.writeStartElement(QLatin1String("Network")); + writer.writeTextElement(QLatin1String("ProxyType"), QString::number(m_settings.proxyType())); + writer.writeStartElement(QLatin1String("Ftp")); + const QNetworkProxy &ftpProxy = m_settings.ftpProxy(); + writer.writeTextElement(QLatin1String("Host"), ftpProxy.hostName()); + writer.writeTextElement(QLatin1String("Port"), QString::number(ftpProxy.port())); + writer.writeTextElement(QLatin1String("Username"), ftpProxy.user()); + writer.writeTextElement(QLatin1String("Password"), ftpProxy.password()); + writer.writeEndElement(); + writer.writeStartElement(QLatin1String("Http")); + const QNetworkProxy &httpProxy = m_settings.httpProxy(); + writer.writeTextElement(QLatin1String("Host"), httpProxy.hostName()); + writer.writeTextElement(QLatin1String("Port"), QString::number(httpProxy.port())); + writer.writeTextElement(QLatin1String("Username"), httpProxy.user()); + writer.writeTextElement(QLatin1String("Password"), httpProxy.password()); + writer.writeEndElement(); + + writer.writeStartElement(QLatin1String("Repositories")); + foreach (const Repository &repo, m_settings.userRepositories()) { + writer.writeStartElement(QLatin1String("Repository")); + writer.writeTextElement(QLatin1String("Host"), repo.url().toString()); + writer.writeTextElement(QLatin1String("Username"), repo.username()); + writer.writeTextElement(QLatin1String("Password"), repo.password()); + writer.writeTextElement(QLatin1String("Enabled"), QString::number(repo.isEnabled())); + writer.writeEndElement(); + } + writer.writeEndElement(); + writer.writeEndElement(); + } +} + +void PackageManagerCorePrivate::readMaintenanceConfigFiles(const QString &targetDir) +{ + QSettingsWrapper cfg(targetDir + QLatin1Char('/') + m_settings.uninstallerIniFile(), + QSettingsWrapper::IniFormat); + const QVariantHash vars = cfg.value(QLatin1String("Variables")).toHash(); + for (QHash<QString, QVariant>::ConstIterator it = vars.constBegin(); it != vars.constEnd(); ++it) + m_vars.insert(it.key(), it.value().toString()); + + QSet<Repository> repos; + const QVariantList variants = cfg.value(QLatin1String("DefaultRepositories")).toList(); + foreach (const QVariant &variant, variants) + repos.insert(variant.value<Repository>()); + if (!repos.isEmpty()) + m_settings.setDefaultRepositories(repos); + + QFile file(targetDir + QLatin1String("/network.xml")); + if (!file.open(QIODevice::ReadOnly)) + return; + + QXmlStreamReader reader(&file); + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: { + if (reader.name() == QLatin1String("Network")) { + while (reader.readNextStartElement()) { + const QStringRef name = reader.name(); + if (name == QLatin1String("Ftp")) { + m_settings.setFtpProxy(readProxy(reader)); + } else if (name == QLatin1String("Http")) { + m_settings.setHttpProxy(readProxy(reader)); + } else if (reader.name() == QLatin1String("Repositories")) { + m_settings.addUserRepositories(readRepositories(reader, false)); + } else if (name == QLatin1String("ProxyType")) { + m_settings.setProxyType(Settings::ProxyType(reader.readElementText().toInt())); + } else { + reader.skipCurrentElement(); + } + } + } + } break; + + case QXmlStreamReader::Invalid: { + qDebug() << reader.errorString(); + } break; + + default: + break; + } + } +} + +void PackageManagerCorePrivate::callBeginInstallation(const QList<Component*> &componentList) +{ + foreach (Component *component, componentList) + component->beginInstallation(); +} + +void PackageManagerCorePrivate::stopProcessesForUpdates(const QList<Component*> &components) +{ + QStringList processList; + foreach (const Component *component, components) + processList << m_core->replaceVariables(component->stopProcessForUpdateRequests()); + + qSort(processList); + processList.erase(std::unique(processList.begin(), processList.end()), processList.end()); + if (processList.isEmpty()) + return; + + while (true) { + const QList<ProcessInfo> allProcesses = runningProcesses(); // FIXME: Unused? + const QStringList processes = checkRunningProcessesFromList(processList); + if (processes.isEmpty()) + return; + + const QMessageBox::StandardButton button = + MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("stopProcessesForUpdates"), tr("Stop Processes"), tr("These processes " + "should be stopped to continue:\n\n%1").arg(QDir::toNativeSeparators(processes + .join(QLatin1String("\n")))), QMessageBox::Retry | QMessageBox::Ignore + | QMessageBox::Cancel, QMessageBox::Retry); + if (button == QMessageBox::Ignore) + return; + if (button == QMessageBox::Cancel) { + m_core->setCanceled(); + throw Error(tr("Installation canceled by user")); + } + } +} + +int PackageManagerCorePrivate::countProgressOperations(const OperationList &operations) +{ + int operationCount = 0; + foreach (Operation *operation, operations) { + if (QObject *operationObject = dynamic_cast<QObject*> (operation)) { + const QMetaObject *const mo = operationObject->metaObject(); + if (mo->indexOfSignal(QMetaObject::normalizedSignature("progressChanged(double)")) > -1) + operationCount++; + } + } + return operationCount; +} + +int PackageManagerCorePrivate::countProgressOperations(const QList<Component*> &components) +{ + int operationCount = 0; + foreach (Component *component, components) + operationCount += countProgressOperations(component->operations()); + + return operationCount; +} + +void PackageManagerCorePrivate::connectOperationToInstaller(Operation *const operation, double operationPartSize) +{ + Q_ASSERT(operationPartSize); + QObject *const operationObject = dynamic_cast< QObject*> (operation); + if (operationObject != 0) { + const QMetaObject *const mo = operationObject->metaObject(); + if (mo->indexOfSignal(QMetaObject::normalizedSignature("outputTextChanged(QString)")) > -1) { + connect(operationObject, SIGNAL(outputTextChanged(QString)), ProgressCoordinator::instance(), + SLOT(emitDetailTextChanged(QString))); + } + + if (mo->indexOfSlot(QMetaObject::normalizedSignature("cancelOperation()")) > -1) + connect(m_core, SIGNAL(installationInterrupted()), operationObject, SLOT(cancelOperation())); + + if (mo->indexOfSignal(QMetaObject::normalizedSignature("progressChanged(double)")) > -1) { + ProgressCoordinator::instance()->registerPartProgress(operationObject, + SIGNAL(progressChanged(double)), operationPartSize); + } + } +} + +Operation *PackageManagerCorePrivate::createPathOperation(const QFileInfo &fileInfo, + const QString &componentName) +{ + const bool isDir = fileInfo.isDir(); + // create an operation with the dir/ file as target, it will get deleted on undo + Operation *operation = createOwnedOperation(QLatin1String(isDir ? "Mkdir" : "Copy")); + if (isDir) + operation->setValue(QLatin1String("createddir"), fileInfo.absoluteFilePath()); + operation->setValue(QLatin1String("component"), componentName); + operation->setArguments(isDir ? QStringList() << fileInfo.absoluteFilePath() + : QStringList() << QString() << fileInfo.absoluteFilePath()); + return operation; +} + +/*! + This creates fake operations which remove stuff which was registered for uninstallation afterwards +*/ +void PackageManagerCorePrivate::registerPathesForUninstallation( + const QList<QPair<QString, bool> > &pathesForUninstallation, const QString &componentName) +{ + if (pathesForUninstallation.isEmpty()) + return; + + QList<QPair<QString, bool> >::const_iterator it; + for (it = pathesForUninstallation.begin(); it != pathesForUninstallation.end(); ++it) { + const bool wipe = it->second; + const QString path = replaceVariables(it->first); + + const QFileInfo fi(path); + // create a copy operation with the file as target -> it will get deleted on undo + Operation *op = createPathOperation(fi, componentName); + if (fi.isDir()) + op->setValue(QLatin1String("forceremoval"), wipe ? scTrue : scFalse); + addPerformed(takeOwnedOperation(op)); + + // get recursive afterwards + if (fi.isDir() && !wipe) { + QDirIterator dirIt(path, QDir::Hidden | QDir::AllEntries | QDir::System + | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (dirIt.hasNext()) { + dirIt.next(); + op = createPathOperation(dirIt.fileInfo(), componentName); + addPerformed(takeOwnedOperation(op)); + } + } + } +} + +void PackageManagerCorePrivate::writeUninstallerBinary(QFile *const input, qint64 size, bool writeBinaryLayout) +{ + QString uninstallerRenamedName = uninstallerName() + QLatin1String(".new"); + qDebug() << "Writing uninstaller:" << uninstallerRenamedName; + + KDSaveFile out(uninstallerRenamedName); + openForWrite(&out, out.fileName()); // throws an exception in case of error + + if (!input->seek(0)) + throw Error(QObject::tr("Failed to seek in file %1: %2").arg(input->fileName(), input->errorString())); + + appendData(&out, input, size); + if (writeBinaryLayout) { + appendInt64(&out, 0); // resource count + appendInt64(&out, 4 * sizeof(qint64)); // data block size + appendInt64(&out, QInstaller::MagicUninstallerMarker); + appendInt64(&out, QInstaller::MagicCookie); + } + out.setPermissions(out.permissions() | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther + | QFile::ExeOther | QFile::ExeGroup | QFile::ExeUser); + + if (!out.commit(KDSaveFile::OverwriteExistingFile)) + throw Error(tr("Could not write uninstaller to %1: %2").arg(out.fileName(), out.errorString())); +} + +void PackageManagerCorePrivate::writeUninstallerBinaryData(QIODevice *output, QFile *const input, + const OperationList &performedOperations, const BinaryLayout &layout) +{ + const qint64 dataBlockStart = output->pos(); + + QVector<Range<qint64> >resourceSegments; + foreach (const Range<qint64> &segment, layout.metadataResourceSegments) { + input->seek(segment.start()); + resourceSegments.append(Range<qint64>::fromStartAndLength(output->pos(), segment.length())); + appendData(output, input, segment.length()); + } + + const qint64 operationsStart = output->pos(); + appendInt64(output, performedOperations.count()); + foreach (Operation *operation, performedOperations) { + // the installer can't be put into XML, remove it first + operation->clearValue(QLatin1String("installer")); + + appendString(output, operation->name()); + appendString(output, operation->toXml().toString()); + + // for the ui not to get blocked + qApp->processEvents(); + } + appendInt64(output, performedOperations.count()); + const qint64 operationsEnd = output->pos(); + + // we don't save any component-indexes. + const qint64 numComponents = 0; + appendInt64(output, numComponents); // for the indexes + // we don't save any components. + const qint64 compIndexStart = output->pos(); + appendInt64(output, numComponents); // and 2 times number of components, + appendInt64(output, numComponents); // one before and one after the components + const qint64 compIndexEnd = output->pos(); + + appendInt64Range(output, Range<qint64>::fromStartAndEnd(compIndexStart, compIndexEnd) + .moved(-dataBlockStart)); + foreach (const Range<qint64> segment, resourceSegments) + appendInt64Range(output, segment.moved(-dataBlockStart)); + appendInt64Range(output, Range<qint64>::fromStartAndEnd(operationsStart, operationsEnd) + .moved(-dataBlockStart)); + appendInt64(output, layout.resourceCount); + //data block size, from end of .exe to end of file + appendInt64(output, output->pos() + 3 * sizeof(qint64) - dataBlockStart); + appendInt64(output, MagicUninstallerMarker); +} + +void PackageManagerCorePrivate::writeUninstaller(OperationList performedOperations) +{ + bool gainedAdminRights = false; + QTemporaryFile tempAdminFile(targetDir() + QLatin1String("/testjsfdjlkdsjflkdsjfldsjlfds") + + QString::number(qrand() % 1000)); + if (!tempAdminFile.open() || !tempAdminFile.isWritable()) { + m_core->gainAdminRights(); + gainedAdminRights = true; + } + + const QString targetAppDirPath = QFileInfo(uninstallerName()).path(); + if (!QDir().exists(targetAppDirPath)) { + // create the directory containing the uninstaller (like a bundle structor, on Mac...) + Operation *op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << targetAppDirPath); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + performedOperations.append(takeOwnedOperation(op)); + } + + writeMaintenanceConfigFiles(); + +#ifdef Q_WS_MAC + // if it is a bundle, we need some stuff in it... + const QString sourceAppDirPath = QCoreApplication::applicationDirPath(); + if (isInstaller() && QFileInfo(sourceAppDirPath + QLatin1String("/../..")).isBundle()) { + Operation *op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << (sourceAppDirPath + QLatin1String("/../PkgInfo")) + << (targetAppDirPath + QLatin1String("/../PkgInfo"))); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + // copy Info.plist to target directory + op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << (sourceAppDirPath + QLatin1String("/../Info.plist")) + << (targetAppDirPath + QLatin1String("/../Info.plist"))); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + // patch the Info.plist after copying it + QFile sourcePlist(sourceAppDirPath + QLatin1String("/../Info.plist")); + openForRead(&sourcePlist, sourcePlist.fileName()); + QFile targetPlist(targetAppDirPath + QLatin1String("/../Info.plist")); + openForWrite(&targetPlist, targetPlist.fileName()); + + QTextStream in(&sourcePlist); + QTextStream out(&targetPlist); + const QString before = QLatin1String("<string>") + QFileInfo(QCoreApplication::applicationFilePath()) + .baseName() + QLatin1String("</string>"); + const QString after = QLatin1String("<string>") + QFileInfo(uninstallerName()).baseName() + + QLatin1String("</string>"); + while (!in.atEnd()) + out << in.readLine().replace(before, after) << endl; + + // copy qt_menu.nib if it exists + op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << (targetAppDirPath + QLatin1String("/../Resources/qt_menu.nib"))); + if (!op->performOperation()) { + qDebug() << "ERROR in Mkdir operation:" << op->errorString(); + } + + op = createOwnedOperation(QLatin1String("CopyDirectory")); + op->setArguments(QStringList() << (sourceAppDirPath + QLatin1String("/../Resources/qt_menu.nib")) + << (targetAppDirPath + QLatin1String("/../Resources/qt_menu.nib"))); + performOperationThreaded(op); + + op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << (QFileInfo(targetAppDirPath).path() + QLatin1String("/Resources"))); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + // copy application icons if it exists + const QString icon = QFileInfo(QCoreApplication::applicationFilePath()).baseName() + + QLatin1String(".icns"); + op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << (sourceAppDirPath + QLatin1String("/../Resources/") + icon) + << (targetAppDirPath + QLatin1String("/../Resources/") + icon)); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + // finally, copy everything within Frameworks and plugins + op = createOwnedOperation(QLatin1String("CopyDirectory")); + op->setArguments(QStringList() << (sourceAppDirPath + QLatin1String("/../Frameworks")) + << (targetAppDirPath + QLatin1String("/../Frameworks"))); + performOperationThreaded(op); + + op = createOwnedOperation(QLatin1String("CopyDirectory")); + op->setArguments(QStringList() << (sourceAppDirPath + QLatin1String("/../plugins")) + << (targetAppDirPath + QLatin1String("/../plugins"))); + performOperationThreaded(op); + } +#endif + + try { + // 1 - check if we have a installer base replacement + // |--- if so, write out the new tool and remove the replacement + // |--- remember to restart and that we need to replace the original binary + // + // 2 - if we do not have a replacement, try to open the binary data file as input + // |--- try to read the binary layout + // |--- on error (see 2.1) + // |--- remember we might to append uncompressed resource data (see 3) + // |--- set the installer or maintenance binary as input to take over binary data + // |--- in case we did not have a replacement, write out an new maintenance tool binary + // |--- remember that we need to replace the original binary + // + // 3 - open a new binary data file + // |--- try to write out the binary data based on the loaded input file (see 2) + // |--- on error (see 3.1) + // |--- if we wrote a new maintenance tool, take this as output - if not, write out a new + // one and set it as output file, remember we did this + // |--- append the binary data based on the loaded input file (see 2), make sure we force + // uncompressing the resource section if we read from a binary data file (see 4.1). + // + // 4 - force a deferred rename on the .dat file (see 4.1) + // 5 - force a deferred rename on the maintenance file (see 5.1) + + // 2.1 - Error cases are: no data file (in fact we are the installer or an old installation), + // could not find the data file magic cookie (unknown .dat file), failed to read binary + // layout (mostly likely the resource section or we couldn't seek inside the file) + // + // 3.1 - most likely the commit operation will fail + // 4.1 - if 3 failed, this makes sure the .dat file will get removed and on the next run all + // binary data is read from the maintenance tool, otherwise it get replaced be the new one + // 5.1 - this will only happen -if- we wrote out a new binary + + bool newBinaryWritten = false; + bool replacementExists = false; + const QString installerBaseBinary = m_core->replaceVariables(m_installerBaseBinaryUnreplaced); + if (!installerBaseBinary.isEmpty() && QFileInfo(installerBaseBinary).exists()) { + qDebug() << "Got a replacement installer base binary:" << installerBaseBinary; + + QFile replacementBinary(installerBaseBinary); + try { + openForRead(&replacementBinary, replacementBinary.fileName()); + writeUninstallerBinary(&replacementBinary, replacementBinary.size(), true); + + m_forceRestart = true; + newBinaryWritten = true; + replacementExists = true; + } catch (const Error &error) { + qDebug() << error.message(); + } + + if (!replacementBinary.remove()) { + // Is there anything more sensible we can do with this error? I think not. It's not serious + // enough for throwing/ aborting the process. + qDebug() << QString::fromLatin1("Could not remove installer base binary (%1) after updating " + "the uninstaller: %2").arg(installerBaseBinary, replacementBinary.errorString()); + } + m_installerBaseBinaryUnreplaced.clear(); + } + + QFile input; + BinaryLayout layout; + const QString dataFile = targetDir() + QLatin1Char('/') + m_settings.uninstallerName() + + QLatin1String(".dat"); + try { + if (isInstaller()) { + throw Error(tr("Found a binary data file, but we are the installer and we should read the " + "binary resource from our very own binary!")); + } + input.setFileName(dataFile); + openForRead(&input, input.fileName()); + layout = BinaryContent::readBinaryLayout(&input, findMagicCookie(&input, MagicCookieDat)); + } catch (const Error &/*error*/) { + input.setFileName(isInstaller() ? installerBinaryPath() : uninstallerName()); + openForRead(&input, input.fileName()); + layout = BinaryContent::readBinaryLayout(&input, findMagicCookie(&input, MagicCookie)); + if (!newBinaryWritten) { + newBinaryWritten = true; + writeUninstallerBinary(&input, layout.endOfData - layout.dataBlockSize, true); + } + } + + try { + KDSaveFile file(dataFile + QLatin1String(".new")); + openForWrite(&file, file.fileName()); + writeUninstallerBinaryData(&file, &input, performedOperations, layout); + appendInt64(&file, MagicCookieDat); + file.setPermissions(file.permissions() | QFile::WriteUser | QFile::ReadGroup + | QFile::ReadOther); + if (!file.commit(KDSaveFile::OverwriteExistingFile)) { + throw Error(tr("Could not write uninstaller binary data to %1: %2").arg(file.fileName(), + file.errorString())); + } + } catch (const Error &/*error*/) { + if (!newBinaryWritten) { + newBinaryWritten = true; + QFile tmp(isInstaller() ? installerBinaryPath() : uninstallerName()); + openForRead(&tmp, tmp.fileName()); + BinaryLayout tmpLayout = BinaryContent::readBinaryLayout(&tmp, findMagicCookie(&tmp, MagicCookie)); + writeUninstallerBinary(&tmp, tmpLayout.endOfData - tmpLayout.dataBlockSize, false); + } + + QFile file(uninstallerName() + QLatin1String(".new")); + openForAppend(&file, file.fileName()); + file.seek(file.size()); + writeUninstallerBinaryData(&file, &input, performedOperations, layout); + appendInt64(&file, MagicCookie); + } + input.close(); + deferredRename(dataFile + QLatin1String(".new"), dataFile, false); + + if (newBinaryWritten) { + const bool restart = replacementExists && isUpdater() && (!statusCanceledOrFailed()); + deferredRename(uninstallerName() + QLatin1String(".new"), uninstallerName(), restart); + qDebug() << "Maintenance tool restart:" << (restart ? "true." : "false."); + } + } catch (const Error &err) { + setStatus(PackageManagerCore::Failure); + if (gainedAdminRights) + m_core->dropAdminRights(); + m_needToWriteUninstaller = false; + throw err; + } + + if (gainedAdminRights) + m_core->dropAdminRights(); + + commitSessionOperations(); + + m_needToWriteUninstaller = false; +} + +QString PackageManagerCorePrivate::registerPath() const +{ +#ifdef Q_OS_WIN + QString productName = m_vars.value(QLatin1String("ProductName")); + if (productName.isEmpty()) + throw Error(tr("ProductName should be set")); + + QString path = QLatin1String("HKEY_CURRENT_USER"); + if (m_vars.value(QLatin1String("AllUsers")) == scTrue) + path = QLatin1String("HKEY_LOCAL_MACHINE"); + + return path + QLatin1String("\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\") + + productName; +#endif + return QString(); +} + +void PackageManagerCorePrivate::runInstaller() +{ + bool adminRightsGained = false; + try { + setStatus(PackageManagerCore::Running); + emit installationStarted(); //resets also the ProgressCoordninator + + //to have some progress for writeUninstaller + ProgressCoordinator::instance()->addReservePercentagePoints(1); + + static const QLatin1String sep("/"); + const QString target = QDir::cleanPath(targetDir().replace(QRegExp(QLatin1String("\\\\|/")), sep)); + if (target.isEmpty()) + throw Error(tr("Variable 'TargetDir' not set.")); + + if (!QDir(target).exists()) { + const QString &pathToTarget = target.mid(0, target.lastIndexOf(sep)); + if (!QDir(pathToTarget).exists()) { + Operation *pathToTargetOp = createOwnedOperation(QLatin1String("Mkdir")); + pathToTargetOp->setArguments(QStringList() << pathToTarget); + if (!performOperationThreaded(pathToTargetOp)) { + adminRightsGained = m_core->gainAdminRights(); + if (!performOperationThreaded(pathToTargetOp)) + throw Error(pathToTargetOp->errorString()); + } + } + } else if (QDir(target).exists()) { + QTemporaryFile tempAdminFile(target + QLatin1String("/adminrights")); + if (!tempAdminFile.open() || !tempAdminFile.isWritable()) + adminRightsGained = m_core->gainAdminRights(); + } + + // add the operation to create the target directory + Operation *mkdirOp = createOwnedOperation(QLatin1String("Mkdir")); + mkdirOp->setArguments(QStringList() << target); + mkdirOp->setValue(QLatin1String("forceremoval"), true); + mkdirOp->setValue(QLatin1String("uninstall-only"), true); + + performOperationThreaded(mkdirOp, Backup); + if (!performOperationThreaded(mkdirOp)) { + // if we cannot create the target dir, we try to activate the admin rights + adminRightsGained = m_core->gainAdminRights(); + if (!performOperationThreaded(mkdirOp)) + throw Error(mkdirOp->errorString()); + } + const QString remove = m_core->value(scRemoveTargetDir); + if (QVariant(remove).toBool()) + addPerformed(takeOwnedOperation(mkdirOp)); + + // to show that there was some work + ProgressCoordinator::instance()->addManualPercentagePoints(1); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Preparing the installation...")); + + const QList<Component*> componentsToInstall = m_core->orderedComponentsToInstall(); + qDebug() << "Install size:" << componentsToInstall.size() << "components"; + + if (!adminRightsGained) { + foreach (Component *component, m_core->orderedComponentsToInstall()) { + if (component->value(scRequiresAdminRights, scFalse) == scFalse) + continue; + + m_core->gainAdminRights(); + m_core->dropAdminRights(); + break; + } + } + + const double downloadPartProgressSize = double(1) / double(3); + double componentsInstallPartProgressSize = double(2) / double(3); + const int downloadedArchivesCount = m_core->downloadNeededArchives(downloadPartProgressSize); + + // if there was no download we have the whole progress for installing components + if (!downloadedArchivesCount) + componentsInstallPartProgressSize = double(1); + + // Force an update on the components xml as the install dir might have changed. + KDUpdater::PackagesInfo &info = *m_updaterApplication.packagesInfo(); + info.setFileName(componentsXmlPath()); + // Clear the packages as we might install into an already existing installation folder. + info.clearPackageInfoList(); + // also update the application name and version, might be set from a script as well + info.setApplicationName(m_core->value(QLatin1String("ProductName"), m_settings.applicationName())); + info.setApplicationVersion(m_core->value(QLatin1String("ProductVersion"), + m_settings.applicationVersion())); + + callBeginInstallation(componentsToInstall); + stopProcessesForUpdates(componentsToInstall); + + const int progressOperationCount = countProgressOperations(componentsToInstall) + + (m_createLocalRepositoryFromBinary ? 1 : 0); // add one more operation as we support progress + double progressOperationSize = componentsInstallPartProgressSize / progressOperationCount; + + foreach (Component *component, componentsToInstall) + installComponent(component, progressOperationSize, adminRightsGained); + + if (m_createLocalRepositoryFromBinary) { + emit m_core->titleMessageChanged(tr("Creating local repository")); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(QString()); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Creating local repository")); + + Operation *createRepo = createOwnedOperation(QLatin1String("CreateLocalRepository")); + if (createRepo) { + createRepo->setValue(QLatin1String("uninstall-only"), true); + createRepo->setValue(QLatin1String("installer"), QVariant::fromValue(m_core)); + createRepo->setArguments(QStringList() << QCoreApplication::applicationFilePath() << target); + + connectOperationToInstaller(createRepo, progressOperationSize); + + bool success = performOperationThreaded(createRepo); + if (!success) { + adminRightsGained = m_core->gainAdminRights(); + success = performOperationThreaded(createRepo); + } + + if (success) { + QSet<Repository> repos; + foreach (Repository repo, m_settings.defaultRepositories()) { + repo.setEnabled(false); + repos.insert(repo); + } + repos.insert(Repository(QUrl::fromUserInput(createRepo + ->value(QLatin1String("local-repo")).toString()), true)); + m_settings.setDefaultRepositories(repos); + addPerformed(takeOwnedOperation(createRepo)); + } else { + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), createRepo->errorString()); + createRepo->undoOperation(); + } + } + } + + emit m_core->titleMessageChanged(tr("Creating Uninstaller")); + + writeUninstaller(m_performedOperationsOld + m_performedOperationsCurrentSession); + registerUninstaller(); + + // fake a possible wrong value to show a full progress bar + const int progress = ProgressCoordinator::instance()->progressInPercentage(); + if (progress < 100) + ProgressCoordinator::instance()->addManualPercentagePoints(100 - progress); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nInstallation finished!")); + + if (adminRightsGained) + m_core->dropAdminRights(); + setStatus(PackageManagerCore::Success); + emit installationFinished(); + } catch (const Error &err) { + if (m_core->status() != PackageManagerCore::Canceled) { + setStatus(PackageManagerCore::Failure); + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + qDebug() << "ROLLING BACK operations=" << m_performedOperationsCurrentSession.count(); + } + + m_core->rollBackInstallation(); + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nInstallation aborted!")); + if (adminRightsGained) + m_core->dropAdminRights(); + emit installationFinished(); + + throw; + } +} + +void PackageManagerCorePrivate::runPackageUpdater() +{ + bool adminRightsGained = false; + try { + if (m_completeUninstall) { + runUninstaller(); + return; + } + + setStatus(PackageManagerCore::Running); + emit installationStarted(); //resets also the ProgressCoordninator + + //to have some progress for the cleanup/write component.xml step + ProgressCoordinator::instance()->addReservePercentagePoints(1); + + const QString packagesXml = componentsXmlPath(); + // check if we need admin rights and ask before the action happens + if (!QFileInfo(installerBinaryPath()).isWritable() || !QFileInfo(packagesXml).isWritable()) + adminRightsGained = m_core->gainAdminRights(); + + const QList<Component *> componentsToInstall = m_core->orderedComponentsToInstall(); + qDebug() << "Install size:" << componentsToInstall.size() << "components"; + + bool updateAdminRights = false; + if (!adminRightsGained) { + foreach (Component *component, componentsToInstall) { + if (component->value(scRequiresAdminRights, scFalse) == scFalse) + continue; + + updateAdminRights = true; + break; + } + } + + OperationList undoOperations; + OperationList nonRevertedOperations; + QHash<QString, Component *> componentsByName; + + // build a list of undo operations based on the checked state of the component + foreach (Operation *operation, m_performedOperationsOld) { + const QString &name = operation->value(QLatin1String("component")).toString(); + Component *component = componentsByName.value(name, 0); + if (!component) + component = m_core->componentByName(name); + if (component) + componentsByName.insert(name, component); + + if (isUpdater()) { + // We found the component, the component is not scheduled for update, the dependency solver + // did not add the component as install dependency and there is no replacement, keep it. + if ((component && !component->updateRequested() && !componentsToInstall.contains(component) + && !m_componentsToReplaceUpdaterMode.contains(name))) { + nonRevertedOperations.append(operation); + continue; + } + + // There is a replacement, but the replacement is not scheduled for update, keep it as well. + if (m_componentsToReplaceUpdaterMode.contains(name) + && !m_componentsToReplaceUpdaterMode.value(name).first->updateRequested()) { + nonRevertedOperations.append(operation); + continue; + } + } else if (isPackageManager()) { + // We found the component, the component is still checked and the dependency solver did not + // add the component as install dependency, keep it. + if (component && component->isSelected() && !componentsToInstall.contains(component)) { + nonRevertedOperations.append(operation); + continue; + } + + // There is a replacement, but the replacement is not scheduled for update, keep it as well. + if (m_componentsToReplaceAllMode.contains(name) + && !m_componentsToReplaceAllMode.value(name).first->installationRequested()) { + nonRevertedOperations.append(operation); + continue; + } + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Invalid package manager mode!"); + } + + // Filter out the create target dir undo operation, it's only needed for full uninstall. + // Note: We filter for unnamed operations as well, since old installations had the remove target + // dir operation without the "uninstall-only", which will result in an complete uninstallation + // during an update for the maintenance tool. + if (operation->value(QLatin1String("uninstall-only")).toBool() + || operation->value(QLatin1String("component")).toString().isEmpty()) { + nonRevertedOperations.append(operation); + continue; + } + + undoOperations.prepend(operation); + updateAdminRights |= operation->value(QLatin1String("admin")).toBool(); + } + + // we did not request admin rights till we found out that a component/ undo needs admin rights + if (updateAdminRights && !adminRightsGained) { + m_core->gainAdminRights(); + m_core->dropAdminRights(); + } + + double undoOperationProgressSize = 0; + const double downloadPartProgressSize = double(2) / double(5); + double componentsInstallPartProgressSize = double(3) / double(5); + if (undoOperations.count() > 0) { + undoOperationProgressSize = double(1) / double(5); + componentsInstallPartProgressSize = downloadPartProgressSize; + undoOperationProgressSize /= countProgressOperations(undoOperations); + } + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Preparing the installation...")); + + // following, we download the needed archives + m_core->downloadNeededArchives(downloadPartProgressSize); + + if (undoOperations.count() > 0) { + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Removing deselected components...")); + runUndoOperations(undoOperations, undoOperationProgressSize, adminRightsGained, true); + } + m_performedOperationsOld = nonRevertedOperations; // these are all operations left: those not reverted + + callBeginInstallation(componentsToInstall); + stopProcessesForUpdates(componentsToInstall); + + const double progressOperationCount = countProgressOperations(componentsToInstall); + const double progressOperationSize = componentsInstallPartProgressSize / progressOperationCount; + + foreach (Component *component, componentsToInstall) + installComponent(component, progressOperationSize, adminRightsGained); + + emit m_core->titleMessageChanged(tr("Creating Uninstaller")); + + commitSessionOperations(); //end session, move ops to "old" + m_needToWriteUninstaller = true; + + // fake a possible wrong value to show a full progress bar + const int progress = ProgressCoordinator::instance()->progressInPercentage(); + if (progress < 100) + ProgressCoordinator::instance()->addManualPercentagePoints(100 - progress); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nUpdate finished!")); + + if (adminRightsGained) + m_core->dropAdminRights(); + setStatus(PackageManagerCore::Success); + emit installationFinished(); + } catch (const Error &err) { + if (m_core->status() != PackageManagerCore::Canceled) { + setStatus(PackageManagerCore::Failure); + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + qDebug() << "ROLLING BACK operations=" << m_performedOperationsCurrentSession.count(); + } + + m_core->rollBackInstallation(); + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nUpdate aborted!")); + if (adminRightsGained) + m_core->dropAdminRights(); + emit installationFinished(); + + throw; + } +} + +void PackageManagerCorePrivate::runUninstaller() +{ + bool adminRightsGained = false; + try { + setStatus(PackageManagerCore::Running); + emit uninstallationStarted(); + + // check if we need administration rights and ask before the action happens + if (!QFileInfo(installerBinaryPath()).isWritable()) + adminRightsGained = m_core->gainAdminRights(); + + OperationList undoOperations; + bool updateAdminRights = false; + foreach (Operation *op, m_performedOperationsOld) { + undoOperations.prepend(op); + updateAdminRights |= op->value(QLatin1String("admin")).toBool(); + } + + // we did not request administration rights till we found out that a undo needs administration rights + if (updateAdminRights && !adminRightsGained) { + m_core->gainAdminRights(); + m_core->dropAdminRights(); + } + + const int uninstallOperationCount = countProgressOperations(undoOperations); + const double undoOperationProgressSize = double(1) / double(uninstallOperationCount); + + //yes uninstallation is like an update on the component so please inform the user to stop processes + callBeginInstallation(m_core->availableComponents()); + stopProcessesForUpdates(m_core->availableComponents()); + + runUndoOperations(undoOperations, undoOperationProgressSize, adminRightsGained, false); + // No operation delete here, as all old undo operations are deleted in the destructor. + + const QString startMenuDir = m_vars.value(scStartMenuDir); + if (!startMenuDir.isEmpty()) { + try { + QInstaller::removeDirectory(startMenuDir); + } catch (const Error &error) { + qDebug() << QString::fromLatin1("Could not remove %1: %2").arg(startMenuDir, error.message()); + } + } else { + qDebug() << "Start menu dir not set."; + } + + // this will also delete the TargetDir on Windows + deleteUninstaller(); + + if (QVariant(m_core->value(scRemoveTargetDir)).toBool()) { + // on !Windows, we need to remove TargetDir manually + qDebug() << "Complete uninstallation is chosen"; + const QString target = targetDir(); + if (!target.isEmpty()) { + if (updateAdminRights && !adminRightsGained) { + // we were root at least once, so we remove the target dir as root + m_core->gainAdminRights(); + removeDirectoryThreaded(target, true); + m_core->dropAdminRights(); + } else { + removeDirectoryThreaded(target, true); + } + } + } + + unregisterUninstaller(); + m_needToWriteUninstaller = false; + + setStatus(PackageManagerCore::Success); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nDeinstallation finished!")); + if (adminRightsGained) + m_core->dropAdminRights(); + emit uninstallationFinished(); + } catch (const Error &err) { + if (m_core->status() != PackageManagerCore::Canceled) { + setStatus(PackageManagerCore::Failure); + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + } + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nDeinstallation aborted!")); + if (adminRightsGained) + m_core->dropAdminRights(); + emit uninstallationFinished(); + + throw; + } +} + +void PackageManagerCorePrivate::installComponent(Component *component, double progressOperationSize, + bool adminRightsGained) +{ + const OperationList operations = component->operations(); + if (!component->operationsCreatedSuccessfully()) + m_core->setCanceled(); + + const int opCount = operations.count(); + // show only components which do something, MinimumProgress is only for progress calculation safeness + if (opCount > 1 || (opCount == 1 && operations.at(0)->name() != QLatin1String("MinimumProgress"))) { + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("\nInstalling component %1") + .arg(component->displayName())); + } + + foreach (Operation *operation, operations) { + if (statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + + // maybe this operations wants us to be admin... + bool becameAdmin = false; + if (!adminRightsGained && operation->value(QLatin1String("admin")).toBool()) { + becameAdmin = m_core->gainAdminRights(); + qDebug() << operation->name() << "as admin:" << becameAdmin; + } + + connectOperationToInstaller(operation, progressOperationSize); + connectOperationCallMethodRequest(operation); + + // allow the operation to backup stuff before performing the operation + PackageManagerCorePrivate::performOperationThreaded(operation, PackageManagerCorePrivate::Backup); + + bool ignoreError = false; + bool ok = PackageManagerCorePrivate::performOperationThreaded(operation); + while (!ok && !ignoreError && m_core->status() != PackageManagerCore::Canceled) { + qDebug() << QString::fromLatin1("Operation '%1' with arguments: '%2' failed: %3") + .arg(operation->name(), operation->arguments().join(QLatin1String("; ")), + operation->errorString()); + const QMessageBox::StandardButton button = + MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationErrorWithRetry"), tr("Installer Error"), + tr("Error during installation process (%1):\n%2").arg(component->name(), + operation->errorString()), + QMessageBox::Retry | QMessageBox::Ignore | QMessageBox::Cancel, QMessageBox::Retry); + + if (button == QMessageBox::Retry) + ok = PackageManagerCorePrivate::performOperationThreaded(operation); + else if (button == QMessageBox::Ignore) + ignoreError = true; + else if (button == QMessageBox::Cancel) + m_core->interrupt(); + } + + if (ok || operation->error() > Operation::InvalidArguments) { + // Remember that the operation was performed, what allows us to undo it if a following operation + // fails or if this operation failed but still needs an undo call to cleanup. + addPerformed(operation); + operation->setValue(QLatin1String("component"), component->name()); + } + + if (becameAdmin) + m_core->dropAdminRights(); + + if (!ok && !ignoreError) + throw Error(operation->errorString()); + + if (component->value(scEssential, scFalse) == scTrue) + m_forceRestart = true; + } + + registerPathesForUninstallation(component->pathesForUninstallation(), component->name()); + + if (!component->stopProcessForUpdateRequests().isEmpty()) { + Operation *stopProcessForUpdatesOp = KDUpdater::UpdateOperationFactory::instance() + .create(QLatin1String("FakeStopProcessForUpdate")); + const QStringList arguments(component->stopProcessForUpdateRequests().join(QLatin1String(","))); + stopProcessForUpdatesOp->setArguments(arguments); + addPerformed(stopProcessForUpdatesOp); + stopProcessForUpdatesOp->setValue(QLatin1String("component"), component->name()); + } + + // now mark the component as installed + KDUpdater::PackagesInfo &packages = *m_updaterApplication.packagesInfo(); + packages.installPackage(component->name(), component->value(scVersion), component->value(scDisplayName), + component->value(scDescription), component->dependencies(), component->forcedInstallation(), + component->isVirtual(), component->value(scUncompressedSize).toULongLong(), + component->value(scInheritVersion)); + packages.writeToDisk(); + + component->setInstalled(); + component->markAsPerformedInstallation(); +} + +// -- private + +void PackageManagerCorePrivate::deleteUninstaller() +{ +#ifdef Q_OS_WIN + // Since Windows does not support that the uninstaller deletes itself we have to go with a rather dirty + // hack. What we do is to create a batchfile that will try to remove the uninstaller once per second. Then + // we start that batchfile detached, finished our job and close ourselves. Once that's done the batchfile + // will succeed in deleting our uninstall.exe and, if the installation directory was created but us and if + // it's empty after the uninstall, deletes the installation-directory. + const QString batchfile = QDir::toNativeSeparators(QFileInfo(QDir::tempPath(), + QLatin1String("uninstall.vbs")).absoluteFilePath()); + QFile f(batchfile); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) + throw Error(tr("Cannot prepare uninstall")); + + QTextStream batch(&f); + batch << "Set fso = WScript.CreateObject(\"Scripting.FileSystemObject\")\n"; + batch << "file = WScript.Arguments.Item(0)\n"; + batch << "folderpath = WScript.Arguments.Item(1)\n"; + batch << "Set folder = fso.GetFolder(folderpath)\n"; + batch << "on error resume next\n"; + + batch << "while fso.FileExists(file)\n"; + batch << " fso.DeleteFile(file)\n"; + batch << " WScript.Sleep(1000)\n"; + batch << "wend\n"; +// batch << "if folder.SubFolders.Count = 0 and folder.Files.Count = 0 then\n"; + batch << " Set folder = Nothing\n"; + batch << " fso.DeleteFolder folderpath, true\n"; +// batch << "end if\n"; + batch << "fso.DeleteFile(WScript.ScriptFullName)\n"; + + f.close(); + + QStringList arguments; + arguments << QLatin1String("//Nologo") << batchfile; // execute the batchfile + arguments << QDir::toNativeSeparators(QFileInfo(installerBinaryPath()).absoluteFilePath()); + if (!m_performedOperationsOld.isEmpty()) { + const Operation *const op = m_performedOperationsOld.first(); + if (op->name() == QLatin1String("Mkdir")) // the target directory name + arguments << QDir::toNativeSeparators(QFileInfo(op->arguments().first()).absoluteFilePath()); + } + + if (!QProcessWrapper::startDetached(QLatin1String("cscript"), arguments, QDir::rootPath())) + throw Error(tr("Cannot start uninstall")); +#else + // every other platform has no problem if we just delete ourselves now + QFile uninstaller(QFileInfo(installerBinaryPath()).absoluteFilePath()); + uninstaller.remove(); +# ifdef Q_WS_MAC + const QLatin1String cdUp("/../../.."); + if (QFileInfo(QFileInfo(installerBinaryPath() + cdUp).absoluteFilePath()).isBundle()) { + removeDirectoryThreaded(QFileInfo(installerBinaryPath() + cdUp).absoluteFilePath()); + QFile::remove(QFileInfo(installerBinaryPath() + cdUp).absolutePath() + + QLatin1String("/") + configurationFileName()); + } else +# endif +#endif + { + // finally remove the components.xml, since it still exists now + QFile::remove(QFileInfo(installerBinaryPath()).absolutePath() + QLatin1String("/") + + configurationFileName()); + } +} + +void PackageManagerCorePrivate::registerUninstaller() +{ +#ifdef Q_OS_WIN + QSettingsWrapper settings(registerPath(), QSettingsWrapper::NativeFormat); + settings.setValue(scDisplayName, m_vars.value(QLatin1String("ProductName"))); + settings.setValue(QLatin1String("DisplayVersion"), m_vars.value(QLatin1String("ProductVersion"))); + const QString uninstaller = QDir::toNativeSeparators(uninstallerName()); + settings.setValue(QLatin1String("DisplayIcon"), uninstaller); + settings.setValue(scPublisher, m_vars.value(scPublisher)); + settings.setValue(QLatin1String("UrlInfoAbout"), m_vars.value(QLatin1String("Url"))); + settings.setValue(QLatin1String("Comments"), m_vars.value(scTitle)); + settings.setValue(QLatin1String("InstallDate"), QDateTime::currentDateTime().toString()); + settings.setValue(QLatin1String("InstallLocation"), QDir::toNativeSeparators(targetDir())); + settings.setValue(QLatin1String("UninstallString"), uninstaller); + settings.setValue(QLatin1String("ModifyPath"), uninstaller + QLatin1String(" --manage-packages")); + settings.setValue(QLatin1String("EstimatedSize"), QFileInfo(installerBinaryPath()).size()); + settings.setValue(QLatin1String("NoModify"), 0); + settings.setValue(QLatin1String("NoRepair"), 1); +#endif +} + +void PackageManagerCorePrivate::unregisterUninstaller() +{ +#ifdef Q_OS_WIN + QSettingsWrapper settings(registerPath(), QSettingsWrapper::NativeFormat); + settings.remove(QString()); +#endif +} + +void PackageManagerCorePrivate::runUndoOperations(const OperationList &undoOperations, double progressSize, + bool adminRightsGained, bool deleteOperation) +{ + KDUpdater::PackagesInfo &packages = *m_updaterApplication.packagesInfo(); + try { + foreach (Operation *undoOperation, undoOperations) { + if (statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + + bool becameAdmin = false; + if (!adminRightsGained && undoOperation->value(QLatin1String("admin")).toBool()) + becameAdmin = m_core->gainAdminRights(); + + connectOperationToInstaller(undoOperation, progressSize); + qDebug() << "undo operation=" << undoOperation->name(); + performOperationThreaded(undoOperation, PackageManagerCorePrivate::Undo); + + const QString componentName = undoOperation->value(QLatin1String("component")).toString(); + if (undoOperation->error() != Operation::NoError) { + if (!componentName.isEmpty()) { + bool run = true; + while (run && m_core->status() != PackageManagerCore::Canceled) { + const QMessageBox::StandardButton button = + MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationErrorWithRetry"), tr("Installer Error"), + tr("Error during uninstallation process:\n%1").arg(undoOperation->errorString()), + QMessageBox::Retry | QMessageBox::Ignore, QMessageBox::Retry); + + if (button == QMessageBox::Retry) { + performOperationThreaded(undoOperation, Undo); + if (undoOperation->error() == Operation::NoError) + run = false; + } else if (button == QMessageBox::Ignore) { + run = false; + } + } + } + } + + if (!componentName.isEmpty()) { + Component *component = m_core->componentByName(componentName); + if (!component) + component = componentsToReplace(m_core->runMode()).value(componentName).second; + if (component) { + component->setUninstalled(); + packages.removePackage(component->name()); + } + } + + if (becameAdmin) + m_core->dropAdminRights(); + + if (deleteOperation) + delete undoOperation; + } + } catch (const Error &error) { + packages.writeToDisk(); + throw Error(error.message()); + } catch (...) { + packages.writeToDisk(); + throw Error(tr("Unknown error")); + } + packages.writeToDisk(); +} + +PackagesList PackageManagerCorePrivate::remotePackages() +{ + if (m_updates && m_updateFinder) + return m_updateFinder->updates(); + + m_updates = false; + delete m_updateFinder; + + m_updateFinder = new KDUpdater::UpdateFinder(&m_updaterApplication); + m_updateFinder->setAutoDelete(false); + m_updateFinder->setUpdateType(KDUpdater::PackageUpdate | KDUpdater::NewPackage); + m_updateFinder->run(); + + if (m_updateFinder->updates().isEmpty()) { + setStatus(PackageManagerCore::Failure, tr("Could not retrieve remote tree: %1.") + .arg(m_updateFinder->errorString())); + return PackagesList(); + } + + m_updates = true; + return m_updateFinder->updates(); +} + +/*! + Returns a hash containing the installed package name and it's associated package information. If + the application is running in installer mode or the local components file could not be parsed, the + hash is empty. +*/ +LocalPackagesHash PackageManagerCorePrivate::localInstalledPackages() +{ + LocalPackagesHash installedPackages; + + if (!isInstaller()) { + KDUpdater::PackagesInfo &packagesInfo = *m_updaterApplication.packagesInfo(); + if (!packagesInfo.isValid()) { + packagesInfo.setFileName(componentsXmlPath()); + if (packagesInfo.applicationName().isEmpty()) + packagesInfo.setApplicationName(m_settings.applicationName()); + if (packagesInfo.applicationVersion().isEmpty()) + packagesInfo.setApplicationVersion(m_settings.applicationVersion()); + } + + if (packagesInfo.error() != KDUpdater::PackagesInfo::NoError) + setStatus(PackageManagerCore::Failure, tr("Failure to read packages from: %1.").arg(componentsXmlPath())); + + foreach (const LocalPackage &package, packagesInfo.packageInfos()) { + if (statusCanceledOrFailed()) + break; + installedPackages.insert(package.name, package); + } + } + + return installedPackages; +} + +bool PackageManagerCorePrivate::fetchMetaInformationFromRepositories() +{ + if (m_repoFetched) + return m_repoFetched; + + m_updates = false; + m_repoFetched = false; + m_updateSourcesAdded = false; + + m_repoMetaInfoJob->reset(); + try { + m_repoMetaInfoJob->start(); + m_repoMetaInfoJob->waitForFinished(); + } catch (Error &error) { + setStatus(PackageManagerCore::Failure, tr("Could not retrieve meta information: %1").arg(error.message())); + return m_repoFetched; + } + + if (m_repoMetaInfoJob->isCanceled() || m_repoMetaInfoJob->error() != KDJob::NoError) { + switch (m_repoMetaInfoJob->error()) { + case QInstaller::UserIgnoreError: + break; // we can simply ignore this error, the user knows about it + default: + setStatus(PackageManagerCore::Failure, m_repoMetaInfoJob->errorString()); + return m_repoFetched; + } + } + + m_repoFetched = true; + return m_repoFetched; +} + +bool PackageManagerCorePrivate::addUpdateResourcesFromRepositories(bool parseChecksum) +{ + if (m_updateSourcesAdded) + return m_updateSourcesAdded; + + if (m_repoMetaInfoJob->temporaryDirectories().isEmpty()) { + m_updateSourcesAdded = true; + return m_updateSourcesAdded; + } + + // forces an refresh/ clear on all update sources + m_updaterApplication.updateSourcesInfo()->refresh(); + if (isInstaller()) { + m_updaterApplication.addUpdateSource(m_settings.applicationName(), m_settings.applicationName(), + QString(), QUrl(QLatin1String("resource://metadata/")), 0); + m_updaterApplication.updateSourcesInfo()->setModified(false); + } + + m_updates = false; + m_updateSourcesAdded = false; + + const QString &appName = m_settings.applicationName(); + const QStringList tempDirs = m_repoMetaInfoJob->temporaryDirectories(); + foreach (const QString &tmpDir, tempDirs) { + if (statusCanceledOrFailed()) + return false; + + if (tmpDir.isEmpty()) + continue; + + if (parseChecksum) { + const QString updatesXmlPath = tmpDir + QLatin1String("/Updates.xml"); + QFile updatesFile(updatesXmlPath); + try { + openForRead(&updatesFile, updatesFile.fileName()); + } catch(const Error &e) { + qDebug() << "Error opening Updates.xml:" << e.message(); + setStatus(PackageManagerCore::Failure, tr("Could not add temporary update source information.")); + return false; + } + + int line = 0; + int column = 0; + QString error; + QDomDocument doc; + if (!doc.setContent(&updatesFile, &error, &line, &column)) { + qDebug() << QString::fromLatin1("Parse error in File %4 : %1 at line %2 col %3").arg(error, + QString::number(line), QString::number(column), updatesFile.fileName()); + setStatus(PackageManagerCore::Failure, tr("Could not add temporary update source information.")); + return false; + } + + const QDomNode checksum = doc.documentElement().firstChildElement(QLatin1String("Checksum")); + if (!checksum.isNull()) + m_core->setTestChecksum(checksum.toElement().text().toLower() == scTrue); + } + m_updaterApplication.addUpdateSource(appName, appName, QString(), QUrl::fromLocalFile(tmpDir), 1); + } + m_updaterApplication.updateSourcesInfo()->setModified(false); + + if (m_updaterApplication.updateSourcesInfo()->updateSourceInfoCount() == 0) { + setStatus(PackageManagerCore::Failure, tr("Could not find any update source information.")); + return false; + } + + m_updateSourcesAdded = true; + return m_updateSourcesAdded; +} + +void PackageManagerCorePrivate::realAppendToInstallComponents(Component *component) +{ + if (!component->isInstalled() || component->updateRequested()) { + //remove the checkState method if we don't use selected in scripts + setCheckedState(component, Qt::Checked); + + m_orderedComponentsToInstall.append(component); + m_toInstallComponentIds.insert(component->name()); + } +} + +void PackageManagerCorePrivate::insertInstallReason(Component *component, const QString &reason) +{ + //keep the first reason + if (m_toInstallComponentIdReasonHash.value(component->name()).isEmpty()) + m_toInstallComponentIdReasonHash.insert(component->name(), reason); +} + +bool PackageManagerCorePrivate::appendComponentToUninstall(Component *component) +{ + // remove all already resolved dependees + QSet<Component *> dependees = m_core->dependees(component).toSet().subtract(m_componentsToUninstall); + if (dependees.isEmpty()) { + setCheckedState(component, Qt::Unchecked); + m_componentsToUninstall.insert(component); + return true; + } + + QSet<Component *> dependeesToResolve; + foreach (Component *dependee, dependees) { + if (dependee->isInstalled()) { + // keep them as already resolved + setCheckedState(dependee, Qt::Unchecked); + m_componentsToUninstall.insert(dependee); + // gather possible dependees, keep them to resolve it later + dependeesToResolve.unite(m_core->dependees(dependee).toSet()); + } + } + + bool allResolved = true; + foreach (Component *dependee, dependeesToResolve) + allResolved &= appendComponentToUninstall(dependee); + + return allResolved; +} + +bool PackageManagerCorePrivate::appendComponentsToUninstall(const QList<Component*> &components) +{ + if (components.isEmpty()) { + qDebug() << "components list is empty in" << Q_FUNC_INFO; + return true; + } + + bool allResolved = true; + foreach (Component *component, components) { + if (component->isInstalled()) { + setCheckedState(component, Qt::Unchecked); + m_componentsToUninstall.insert(component); + allResolved &= appendComponentToUninstall(component); + } + } + + QSet<Component*> installedComponents; + foreach (const QString &name, localInstalledPackages().keys()) { + if (Component *component = m_core->componentByName(name)) { + if (!component->uninstallationRequested()) + installedComponents.insert(component); + } + } + + QList<Component*> autoDependOnList; + if (allResolved) { + // All regular dependees are resolved. Now we are looking for auto depend on components. + foreach (Component *component, installedComponents) { + // If a components is installed and not yet scheduled for un-installation, check for auto depend. + if (component->isInstalled() && !m_componentsToUninstall.contains(component)) { + QStringList autoDependencies = component->autoDependencies(); + if (autoDependencies.isEmpty()) + continue; + + // This code needs to be enabled once the scripts use isInstalled, installationRequested and + // uninstallationRequested... + if (autoDependencies.first().compare(QLatin1String("script"), Qt::CaseInsensitive) == 0) { + //QScriptValue valueFromScript; + //try { + // valueFromScript = callScriptMethod(QLatin1String("isAutoDependOn")); + //} catch (const Error &error) { + // // keep the component, should do no harm + // continue; + //} + + //if (valueFromScript.isValid() && !valueFromScript.toBool()) + // autoDependOnList.append(component); + continue; + } + + foreach (Component *c, installedComponents) { + const QString replaces = c->value(scReplaces); + QStringList possibleNames = replaces.split(QRegExp(QLatin1String("\\b(,|, )\\b")), + QString::SkipEmptyParts); + possibleNames.append(c->name()); + foreach (const QString &possibleName, possibleNames) + autoDependencies.removeAll(possibleName); + } + + // A component requested auto installation, keep it to resolve their dependencies as well. + if (!autoDependencies.isEmpty()) + autoDependOnList.append(component); + } + } + } + + if (!autoDependOnList.isEmpty()) + return appendComponentsToUninstall(autoDependOnList); + return allResolved; +} + +void PackageManagerCorePrivate::resetComponentsToUserCheckedState() +{ + if (m_coreCheckedHash.isEmpty()) + return; + + foreach (Component *component, m_coreCheckedHash.keys()) + component->setCheckState(m_coreCheckedHash.value(component)); + + m_coreCheckedHash.clear(); + m_componentsToInstallCalculated = false; +} + +void PackageManagerCorePrivate::setCheckedState(Component *component, Qt::CheckState state) +{ + m_coreCheckedHash.insert(component, component->checkState()); + component->setCheckState(state); +} + +void PackageManagerCorePrivate::connectOperationCallMethodRequest(Operation *const operation) +{ + QObject *const operationObject = dynamic_cast<QObject *> (operation); + if (operationObject != 0) { + const QMetaObject *const mo = operationObject->metaObject(); + if (mo->indexOfSignal(QMetaObject::normalizedSignature("requestBlockingExecution(QString)")) > -1) { + connect(operationObject, SIGNAL(requestBlockingExecution(QString)), + this, SLOT(handleMethodInvocationRequest(QString)), Qt::BlockingQueuedConnection); + } + } +} + +void PackageManagerCorePrivate::handleMethodInvocationRequest(const QString &invokableMethodName) +{ + QObject *obj = QObject::sender(); + if (obj != 0) + QMetaObject::invokeMethod(obj, qPrintable(invokableMethodName)); +} + +} // namespace QInstaller |