diff options
Diffstat (limited to 'installerbuilder/libinstaller/qinstaller.cpp')
-rw-r--r-- | installerbuilder/libinstaller/qinstaller.cpp | 3356 |
1 files changed, 3356 insertions, 0 deletions
diff --git a/installerbuilder/libinstaller/qinstaller.cpp b/installerbuilder/libinstaller/qinstaller.cpp new file mode 100644 index 000000000..c41734979 --- /dev/null +++ b/installerbuilder/libinstaller/qinstaller.cpp @@ -0,0 +1,3356 @@ +/************************************************************************** +** +** This file is part of Qt SDK** +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).* +** +** Contact: Nokia Corporation qt-info@nokia.com** +** +** No Commercial Usage +** +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** 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. +** +** If you are unsure which license is appropriate for your use, please contact +** (qt-info@nokia.com). +** +**************************************************************************/ +#include "qinstaller.h" +#include "qinstallergui.h" +#include "qinstallerglobal.h" +#include "qinstallercomponent.h" +#include "downloadarchivesjob.h" +#include "getrepositoriesmetainfojob.h" +#include "adminauthorization.h" +#include "messageboxhandler.h" +#include "progresscoordinator.h" + +#include "common/installersettings.h" +#include "common/binaryformat.h" +#include "common/utils.h" + +#include "fsengineclient.h" +#include "fsengineserver.h" + +#include <QtCore/QBuffer> +#include <QtCore/QCoreApplication> +#include <QtCore/QDateTime> +#include <QtCore/QDir> +#include <QtCore/QDirIterator> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QHash> +#include <QtCore/QLocale> +#include <QtCore/QPointer> +#include <QtCore/QProcess> +#include <QtCore/QResource> +#include <QtCore/QSettings> +#include <QtCore/QVector> +#include <QtCore/QTemporaryFile> +#include <QtCore/QTranslator> +#include <QtCore/QUrl> +#include <QtCore/QThread> +#include <QtCore/QSet> +#include <QtGui/QDesktopServices> +#include <QtGui/QMessageBox> +#include <QtUiTools/QUiLoader> +#include <QtScript/QScriptEngine> +#include <QtScript/QScriptContext> +#include <QtScript/QScriptContextInfo> +#include <QFuture> +#include <QFutureWatcher> +#include <QtConcurrentRun> + +#include <KDUpdater/KDUpdater> +#include <KDUpdater/Application> +#include <KDUpdater/PackagesInfo> + +#include <KDToolsCore/KDSaveFile> +#include <KDToolsCore/KDSysInfo> +#include <KDToolsCore/KDSelfRestarter> + +#include "common/errors.h" +#include "common/fileutils.h" + +#include <cassert> +#include <cerrno> +#include <functional> +#include <memory> +#include <algorithm> + +#ifdef Q_OS_WIN +#include "qt_windows.h" +#endif + +using namespace QInstaller; + +namespace { + enum OperationType { + Backup, + Perform, + Undo + }; + + static bool runOperation(KDUpdater::UpdateOperation *op, OperationType type) { + switch (type) + { + case(Backup): + op->backup(); + return true; + case (Perform): + return op->performOperation(); + case (Undo) : + return op->undoOperation(); + } + Q_ASSERT(!"unexpected operation type"); + return false; + } + + template <typename T> + void letTheUiRunTillFinished(const QFuture<T>& f) { + QFutureWatcher<T> futureWatcher; + futureWatcher.setFuture(f); + QEventLoop loop; + loop.connect(&futureWatcher, SIGNAL(finished()), SLOT(quit()), Qt::QueuedConnection); + if (!f.isFinished()) + loop.exec(); + } + +} + +static bool performOperationThreaded(KDUpdater::UpdateOperation *op, OperationType type=Perform) +{ + QFuture<bool> future = QtConcurrent::run(runOperation, op, type); + letTheUiRunTillFinished(future); + return future.result(); +} + +static QScriptValue checkArguments(QScriptContext* context, int amin, int amax) +{ + if (context->argumentCount() < amin || context->argumentCount() > amax) { + if (amin != amax) { + return context->throwError(QObject::tr("Invalid arguments: %1 arguments given, %2 to " + "%3 expected.").arg(QString::number(context->argumentCount()), + QString::number(amin), QString::number(amax))); + } + return context->throwError(QObject::tr("Invalid arguments: %1 arguments given, %2 expected.") + .arg(QString::number(context->argumentCount()), QString::number(amin))); + } + return QScriptValue(); +} + +#ifdef Q_WS_WIN +static void deferredRename(const QString& oldName, const QString& newName, bool restart = false) +{ + QString batchfile; + + QStringList arguments; + arguments << QDir::toNativeSeparators(batchfile) + << QDir::toNativeSeparators(oldName) + << QDir::toNativeSeparators(QFileInfo(oldName).dir().absoluteFilePath(newName)); + + { + QTemporaryFile f(QDir::temp().absoluteFilePath(QLatin1String("deferredrenameXXXXXX.vbs"))); + openForWrite(&f, f.fileName()); + f.setAutoRemove(false); + + batchfile = f.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"; + } + + QProcess::startDetached(QLatin1String("cscript"), QStringList() << QLatin1String("//Nologo") + << QDir::toNativeSeparators(batchfile)); +} +#endif // Q_WS_WIN + +/*! + Appends \a comp preceded by its dependencies to \a components. Makes sure components contains + every component only once. + \internal +*/ +static void appendComponentAndMissingDependencies(QList<Component*>& components, Component* comp) +{ + if (comp == 0) + return; + + const QList<Component*> deps = comp->installer()->missingDependencies(comp); + for (QList<Component*>::const_iterator it = deps.begin(); it != deps.end(); ++it) + appendComponentAndMissingDependencies(components, *it); + if (!components.contains(comp)) + components.push_back(comp); +} + +/*! + Scriptable version of Installer::componentByName(QString). + \sa Installer::componentByName + */ +QScriptValue QInstaller::qInstallerComponentByName(QScriptContext* context, QScriptEngine* engine) +{ + const QScriptValue check = checkArguments(context, 1, 1); + if (check.isError()) + return check; + + // well... this is our "this" pointer + Installer* const installer = dynamic_cast< Installer* >(engine->globalObject() + .property(QLatin1String("installer")).toQObject()); + + const QString name = context->argument(0).toString(); + Component* const c = installer->componentByName(name); + return engine->newQObject(c); +} + +QScriptValue QInstaller::qDesktopServicesOpenUrl(QScriptContext* context, QScriptEngine* engine) +{ + Q_UNUSED(engine); + const QScriptValue check = checkArguments(context, 1, 1); + if (check.isError()) + return check; + const QString url = context->argument(0).toString(); + return QDesktopServices::openUrl(QUrl(url)); +} + +QScriptValue QInstaller::qDesktopServicesDisplayName(QScriptContext* context, QScriptEngine* engine) +{ + Q_UNUSED(engine); + const QScriptValue check = checkArguments(context, 1, 1); + if (check.isError()) + return check; + const QDesktopServices::StandardLocation location = + static_cast< QDesktopServices::StandardLocation >(context->argument(0).toInt32()); + return QDesktopServices::displayName(location); +} + +QScriptValue QInstaller::qDesktopServicesStorageLocation(QScriptContext* context, QScriptEngine* engine) +{ + Q_UNUSED(engine); + const QScriptValue check = checkArguments(context, 1, 1); + if (check.isError()) + return check; + const QDesktopServices::StandardLocation location = + static_cast< QDesktopServices::StandardLocation >(context->argument(0).toInt32()); + return QDesktopServices::storageLocation(location); +} + +QString QInstaller::uncaughtExceptionString(QScriptEngine *scriptEngine/*, const QString &context*/) +{ + //QString errorString(QLatin1String("%1 %2\n%3")); + QString errorString(QLatin1String("\t\t%1\n%2")); + //if (!context.isEmpty()) + // errorString.prepend(context + QLatin1String(": ")); + + //usually the linenumber is in the backtrace + errorString = errorString.arg(/*QString::number(scriptEngine->uncaughtExceptionLineNumber()),*/ + scriptEngine->uncaughtException().toString(), + scriptEngine->uncaughtExceptionBacktrace().join(QLatin1String("\n"))); + return errorString; +} + + +/*! + \class QInstaller::Installer + Installer forms the core of the installation and uninstallation system. + */ + +/*! + \enum QInstaller::Installer::WizardPage + WizardPage is used to number the different pages known to the Installer GUI. + */ + +/*! + \var QInstaller::Installer::Introduction + Introduction page. + */ + +/*! + \var QInstaller::Installer::LicenseCheck + License check page + */ +/*! + \var QInstaller::Installer::TargetDirectory + Target directory selection page + */ +/*! + \var QInstaller::Installer::ComponentSelection + %Component selection page + */ +/*! + \var QInstaller::Installer::StartMenuSelection + Start menu directory selection page - Microsoft Windows only + */ +/*! + \var QInstaller::Installer::ReadyForInstallation + "Ready for Installation" page + */ +/*! + \var QInstaller::Installer::PerformInstallation + Page shown while performing the installation + */ +/*! + \var QInstaller::Installer::InstallationFinished + Page shown when the installation was finished + */ +/*! + \var QInstaller::Installer::End + Non-existing page - this value has to be used if you want to insert a page after \a InstallationFinished + */ + +/*! + Initializes the created FSEngineClientHandler instance \a handler. + \internal + */ +static void initEngineHandler(FSEngineClientHandler* handler) +{ +#ifdef FSENGINE_TCP + const int port = 30000 + qrand() % 1000; + handler->init(port); + handler->setStartServerCommand(qApp->applicationFilePath(), QStringList() + << QLatin1String("--startserver") << QString::number(port) << handler->authorizationKey(), + true); +#else + const QString name = QInstaller::generateTemporaryFileName(); + handler->init(name); + handler->setStartServerCommand(qApp->applicationFilePath(), QStringList() + << QLatin1String("--startserver") << name << handler->authorizationKey(), true); +#endif +} + + + +/*! + Creates and initializes a FSEngineClientHandler -> makes us get admin rights for QFile + operations. + \internal + */ +static FSEngineClientHandler* createEngineClientHandler() +{ + static FSEngineClientHandler* clientHandlerInstance = 0; + if (clientHandlerInstance == 0) + { + clientHandlerInstance = new FSEngineClientHandler; + initEngineHandler(clientHandlerInstance); + } + return clientHandlerInstance; +} + + +// -- Installer::Private + + +class QInstaller::Installer::Private : public QObject +{ + Q_OBJECT; + +private: + Installer* const q; + +public: + explicit Private(Installer *q, qint64 magicmaker, + QVector<KDUpdater::UpdateOperation*> performedOperations); + ~Private(); + + void initialize(); + + bool statusCanceledOrFailed() const; + void setStatus(Installer::Status); + + void writeUninstaller(QVector<KDUpdater::UpdateOperation*> performedOperations); + QString targetDir() const { return q->value(QLatin1String("TargetDir")); } + + QString componentsXmlPath() const + { + return QDir::toNativeSeparators(QDir(QDir::cleanPath(targetDir())) + .absoluteFilePath(QLatin1String("components.xml"))); + } + + QString localComponentsXmlPath() const + { + const QString &appDirPath = QCoreApplication::applicationDirPath(); + if (QFileInfo(appDirPath + QLatin1String("/../..")).isBundle()) { + return QDir::toNativeSeparators(QFileInfo(QDir::cleanPath(appDirPath + + QLatin1String("/../../../components.xml"))).absoluteFilePath()); + } + return componentsXmlPath(); + } + + void runInstaller(); + void runUninstaller(); + void runPackageUpdater(); + void deleteUninstaller(); + QString uninstallerName() const; + QString replaceVariables(const QString &str) const; + QByteArray replaceVariables(const QByteArray &str) const; + QString registerPath() const; + void registerInstaller(); + void unregisterInstaller(); + QString installerBinaryPath() const; + bool isInstaller() const; + bool isUninstaller() const; + bool isPackageManager() const; + Installer *installer() const { return q; } + KDUpdater::UpdateOperation* createOwnedOperation(const QString& type); + void readUninstallerIniFile(const QString& targetDir); + void stopProcessesForUpdates(const QList<Component*>& components); + int countProgressOperations(const QList<Component*>& components); + int countProgressOperations(const QList<KDUpdater::UpdateOperation*>& operations); + void connectOperationToInstaller(KDUpdater::UpdateOperation* const operation, + double progressOperationPartSize); + void registerPathesForUninstallation(const QList<QPair<QString, bool> > &pathesForUninstallation, + const QString& componentName); + + void addPerformed(KDUpdater::UpdateOperation* op) { + m_performedOperationsCurrentSession.push_back(op); + } + + void commitSessionOperations() { + m_performedOperationsOld += m_performedOperationsCurrentSession; + m_performedOperationsCurrentSession.clear(); + } + +signals: + void installationStarted(); + void installationFinished(); + void uninstallationStarted(); + void uninstallationFinished(); + +public: + TempDirDeleter tempDirDeleter; + + FSEngineClientHandler* const engineClientHandler; + + QHash<QString, QString> m_vars; + QHash<QString, bool> m_sharedFlags; + Installer::Status m_status; + + bool packageManagingMode; + bool m_completeUninstall; + + qint64 m_firstComponentStart; + qint64 m_componentsCount; + qint64 m_componentOffsetTableStart; + + qint64 m_firstComponentDictStart; + qint64 m_componentsDictCount; + qint64 m_componentDictOffsetTableStart; + + qint64 m_globalDictOffset; + qint64 m_magicInstallerMarker; + + KDUpdater::Application * m_app; + + // Owned. Indexed by component name + QList<Component*> m_components; + QHash<QString, Component*> m_componentHash; + + QList<Component*> m_updaterComponents; + QList<Component*> m_packageManagerComponents; + + QList<KDUpdater::UpdateOperation*> ownedOperations; + QVector<KDUpdater::UpdateOperation*> m_performedOperationsOld; + QVector<KDUpdater::UpdateOperation*> m_performedOperationsCurrentSession; + InstallerSettings m_settings; + QString installerBaseBinaryUnreplaced; + bool linearComponentList; + bool m_launchedAsRoot; + int m_silentRetries; + bool m_forceRestart; + bool m_needToWriteUninstaller; + bool m_testChecksum; +}; + +Installer::Private::Private(Installer *q, qint64 magicmaker, + QVector<KDUpdater::UpdateOperation*> performedOperations) + : q(q), + engineClientHandler(createEngineClientHandler()), + m_status(Installer::InstallerUnfinished), + packageManagingMode(false), + m_completeUninstall(false), + m_magicInstallerMarker(magicmaker),//? magicmaker : MagicInstallerMarker), + m_performedOperationsOld(performedOperations), + linearComponentList(false), + m_launchedAsRoot(AdminAuthorization::hasAdminRights()), + m_silentRetries(3), + m_forceRestart (false), + m_needToWriteUninstaller(false), + m_testChecksum(false) +{ + connect(this, SIGNAL(installationStarted()), q, SIGNAL(installationStarted())); + connect(this, SIGNAL(installationFinished()), q, SIGNAL(installationFinished())); + connect(this, SIGNAL(uninstallationStarted()), q, SIGNAL(uninstallationStarted())); + connect(this, SIGNAL(uninstallationFinished()), q, SIGNAL(uninstallationFinished())); + verbose() << "has admin rights ? : " << engineClientHandler->isActive() << std::endl; +} + +Installer::Private::~Private() +{ + qDeleteAll(m_components); + qDeleteAll(m_updaterComponents); + m_components.clear(); + qDeleteAll(m_performedOperationsOld); + qDeleteAll(m_performedOperationsCurrentSession); +} + +KDUpdater::Application& Installer::updaterApplication() const +{ + return *d->m_app; +} + +void Installer::setUpdaterApplication(KDUpdater::Application *app) +{ + d->m_app = app; +} + +void Installer::writeUninstaller() +{ + if (d->m_needToWriteUninstaller) { + bool error = false; + QString errorMsg; + try { + d->writeUninstaller(d->m_performedOperationsOld + d->m_performedOperationsCurrentSession); + + bool gainedAdminRights = false; + QTemporaryFile tempAdminFile(d->targetDir() + + QLatin1String("/testjsfdjlkdsjflkdsjfldsjlfds") + QString::number(qrand() % 1000)); + if (!tempAdminFile.open() || !tempAdminFile.isWritable()) { + gainAdminRights(); + gainedAdminRights = true; + } + d->m_app->packagesInfo()->writeToDisk(); + if (gainedAdminRights) + dropAdminRights(); + d->m_needToWriteUninstaller = false; + } catch (const Error& e) { + error = true; + errorMsg = e.message(); + } + + if (error) { + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("WriteError"), tr("Error writing Uninstaller"), errorMsg, + QMessageBox::Ok, QMessageBox::Ok); + } + } +} + +void Installer::reset(const QHash<QString, QString> ¶ms) +{ + d->m_completeUninstall = false; + d->m_forceRestart = false; + d->m_status = Installer::InstallerUnfinished; + d->installerBaseBinaryUnreplaced.clear(); + d->m_vars.clear(); + d->m_vars = params; + d->initialize(); +} + +void Installer::Private::initialize() +{ + try { + m_settings = InstallerSettings::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; + } + + // 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()); + +#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 + + // 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(QLatin1String("Title"), m_settings.title()); + m_vars.insert(QLatin1String("MaintenanceTitle"), m_settings.maintenanceTitle()); + m_vars.insert(QLatin1String("Publisher"), m_settings.publisher()); + m_vars.insert(QLatin1String("Url"), m_settings.url()); + m_vars.insert(QLatin1String("StartMenuDir"), m_settings.startMenuDir()); + + 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(QLatin1String("RunProgram"), replaceVariables(m_settings.runProgram())); + const QString desc = m_settings.runProgramDescription(); + if (!desc.isEmpty()) + m_vars.insert(QLatin1String("RunProgramDescription"), desc); +#ifdef Q_WS_X11 + if (m_launchedAsRoot) + m_vars.insert(QLatin1String("TargetDir"), replaceVariables(m_settings.adminTargetDir())); + else +#endif + m_vars.insert(QLatin1String("TargetDir"), replaceVariables(m_settings.targetDir())); + + QSettings creatorSettings(QSettings::IniFormat, QSettings::UserScope, QLatin1String("Nokia"), + QLatin1String("QtCreator")); + QFileInfo info(creatorSettings.fileName()); + if (info.exists()) + m_vars.insert(QLatin1String("QtCreatorSettingsFile"), info.absoluteFilePath()); + + if (!q->isInstaller()) { +#ifdef Q_WS_MAC + readUninstallerIniFile(QCoreApplication::applicationDirPath() + QLatin1String("/../../..")); +#else + readUninstallerIniFile(QCoreApplication::applicationDirPath()); +#endif + } + + connect(this, SIGNAL(installationStarted()), ProgressCoordninator::instance(), SLOT(reset())); + connect(this, SIGNAL(uninstallationStarted()), ProgressCoordninator::instance(), SLOT(reset())); +} + +/*! + * Sets the uninstallation to be \a complete. If \a complete is false, only components deselected + * by the user will be uninstalled. + * This option applies only on uninstallation. + */ +void Installer::setCompleteUninstallation(bool complete) +{ + d->m_completeUninstall = complete; + d->packageManagingMode = !d->m_completeUninstall; +} + +QString Installer::Private::installerBinaryPath() const +{ + return qApp->applicationFilePath(); +} + +bool Installer::Private::isInstaller() const +{ + return m_magicInstallerMarker == MagicInstallerMarker; +} + +bool Installer::Private::isUninstaller() const +{ + return m_magicInstallerMarker == MagicUninstallerMarker && !packageManagingMode; +} + +bool Installer::Private::isPackageManager() const +{ + return m_magicInstallerMarker == MagicUninstallerMarker && packageManagingMode; +} + +bool Installer::Private::statusCanceledOrFailed() const +{ + return m_status == Installer::InstallerCanceledByUser + || m_status == Installer::InstallerFailed; +} + +void Installer::Private::setStatus(Installer::Status status) +{ + if (m_status != status) { + m_status = status; + emit q->statusChanged(m_status); + } +} + +QString Installer::Private::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 += q->value(name); + pos = pos2 + 1; + } + res += str.mid(pos); + return res; +} + +QByteArray Installer::Private::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 += q->value(name).toLocal8Bit(); + pos = pos2 + 1; + } + res += ba.mid(pos); + return res; +} + +/*! + Creates an update operation owned by the installer, not by any component. + \internal + */ +KDUpdater::UpdateOperation* Installer::Private::createOwnedOperation(const QString &type) +{ + KDUpdater::UpdateOperation* const op = KDUpdater::UpdateOperationFactory::instance().create(type); + ownedOperations.push_back(op); + return op; +} + +QString Installer::Private::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); +} + +void Installer::Private::readUninstallerIniFile(const QString &targetDir) +{ + const QString iniPath = targetDir + QLatin1Char('/') + m_settings.uninstallerIniFile(); + QSettings cfg(iniPath, QSettings::IniFormat); + const QVariantHash vars = cfg.value(QLatin1String("Variables")).toHash(); + QHash<QString, QVariant>::ConstIterator it = vars.constBegin(); + while (it != vars.constEnd()) { + m_vars.insert(it.key(), it.value().toString()); + ++it; + } +} + +/*! + Copied from QInstaller with some adjustments + Return true, if a process with \a name is running. On Windows, the comparision is case-insensitive. +*/ +static bool isProcessRunning(const QString& name, const QList<KDSysInfo::ProcessInfo> &processes) +{ + QList<KDSysInfo::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 QStringList checkRunningProcessesFromList(const QStringList &processList) +{ + const QList<KDSysInfo::ProcessInfo> allProcesses = KDSysInfo::runningProcesses(); + QStringList stillRunningProcesses; + foreach (const QString &process, processList) { + if (!process.isEmpty() && isProcessRunning(process, allProcesses)) { + stillRunningProcesses.append(process); + } + } + return stillRunningProcesses; +} + +void Installer::Private::stopProcessesForUpdates(const QList<Component*> &components) +{ + QStringList processList; + foreach (const Component* const i, components) + processList << q->replaceVariables(i->stopProcessForUpdateRequests()); + + qSort(processList); + processList.erase(std::unique(processList.begin(), processList.end()), processList.end()); + if (processList.isEmpty()) + return; + + while (true) { + 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) { + q->setCanceled(); + throw Error(tr("Installation canceled by user")); + } + } +} + +int Installer::Private::countProgressOperations(const QList<KDUpdater::UpdateOperation*> &operations) +{ + int operationCount = 0; + QList<KDUpdater::UpdateOperation*>::const_iterator oIt; + for (oIt = operations.constBegin(); oIt != operations.constEnd(); oIt++) { + KDUpdater::UpdateOperation* const operation = *oIt; + QObject* const operationObject = dynamic_cast< QObject* >(operation); + if (operationObject != 0) { + const QMetaObject* const mo = operationObject->metaObject(); + if (mo->indexOfSignal(QMetaObject::normalizedSignature("progressChanged(double)")) > -1) + operationCount++; + } + } + return operationCount; +} + +int Installer::Private::countProgressOperations(const QList<Component*> &components) +{ + int operationCount = 0; + for (QList<Component*>::const_iterator It = components.begin(); It != components.end(); ++It) + operationCount = operationCount + countProgressOperations((*It)->operations()); + + return operationCount; +} + +void Installer::Private::connectOperationToInstaller(KDUpdater::UpdateOperation* const operation, + double progressOperationPartSize) +{ + Q_ASSERT(progressOperationPartSize); + 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)), + ProgressCoordninator::instance(), SLOT(emitDetailTextChanged(QString))); + } + + if (mo->indexOfSlot(QMetaObject::normalizedSignature("cancelOperation()")) > -1) + connect(q, SIGNAL(installationInterrupted()), operationObject, SLOT(cancelOperation())); + + if (mo->indexOfSignal(QMetaObject::normalizedSignature("progressChanged(double)")) > -1) { + ProgressCoordninator::instance()->registerPartProgress(operationObject, + SIGNAL(progressChanged(double)), progressOperationPartSize); + } + } +} + +/*! + This creates fake operations which remove stuff which was registered for uninstallation afterwards +*/ +void Installer::Private::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 QString path = replaceVariables(it->first); + const bool wipe = it->second; + const QFileInfo fi(path); + + // create a copy operation with the file as target -> it will get deleted on undo + KDUpdater::UpdateOperation* const op = createOwnedOperation(QLatin1String(fi.isDir() + ? "Mkdir" : "Copy")); + if (fi.isDir()) { + op->setValue(QLatin1String("createddir"), fi.absoluteFilePath()); + op->setValue(QLatin1String("forceremoval"), wipe ? QLatin1String("true") + : QLatin1String("false")); + } + op->setArguments(fi.isDir() ? QStringList() << fi.absoluteFilePath() + : QStringList() << QString() << fi.absoluteFilePath()); + op->setValue(QLatin1String("component"), componentName); + addPerformed(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(); + const QFileInfo fi = dirIt.fileInfo(); + if (fi.isDir()) { + // create an mkdir operation with the dir as target -> it will get deleted on undo + KDUpdater::UpdateOperation* const op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << fi.absoluteFilePath()); + op->setValue(QLatin1String("createddir"), fi.absoluteFilePath()); + op->setValue(QLatin1String("component"), componentName); + addPerformed(op); + } else { + // create a copy operation with the file as target -> it will get deleted on undo + KDUpdater::UpdateOperation* const op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << QString() << fi.absoluteFilePath()); + op->setValue(QLatin1String("component"), componentName); + addPerformed(op); + } + } + } + } +} + +void Installer::Private::writeUninstaller(QVector<KDUpdater::UpdateOperation*> performedOperations) +{ + bool gainedAdminRights = false; + QTemporaryFile tempAdminFile(targetDir() + QString::fromLatin1("/testjsfdjlkdsjflkdsjfldsjlfds") + + QString::number(qrand() % 1000)); + if (!tempAdminFile.open() || !tempAdminFile.isWritable()) { + q->gainAdminRights(); + gainedAdminRights = true; + } + + verbose() << "QInstaller::Installer::Private::writeUninstaller uninstaller=" << uninstallerName() + << std::endl; + + // create the directory containing the uninstaller (like a bundle structor, on Mac...) + KDUpdater::UpdateOperation* op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << QFileInfo(uninstallerName()).path()); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + performedOperations.push_back(op); + + { + // write current state (variables) to the uninstaller ini file + const QString iniPath = targetDir() + QLatin1Char('/') + m_settings.uninstallerIniFile(); + QSettings cfg(iniPath, QSettings::IniFormat); + QVariantHash vars; + QHash<QString, QString>::ConstIterator it = m_vars.constBegin(); + while (it != m_vars.constEnd()) { + const QString &key = it.key(); + if (key != QLatin1String("RunProgramDescription") && key != QLatin1String("RunProgram")) + vars.insert(key, it.value()); + ++it; + } + cfg.setValue(QLatin1String("Variables"), vars); + cfg.sync(); + if (cfg.status() != QSettings::NoError) { + const QString reason = cfg.status() == QSettings::AccessError ? tr("Access error") + : tr("Format error"); + throw Error(tr("Could not write installer configuration to %1: %2").arg(iniPath, reason)); + } + } + +#ifdef Q_WS_MAC + // if it is a bundle, we need some stuff in it... + if (isInstaller() + && QFileInfo(QCoreApplication::applicationDirPath() + QLatin1String("/../..")).isBundle()) { + op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << (QCoreApplication::applicationDirPath() + + QLatin1String("/../PkgInfo")) << (QFileInfo(uninstallerName()).path() + + QLatin1String("/../PkgInfo"))); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << (QCoreApplication::applicationDirPath() + + QLatin1String("/../Info.plist")) << (QFileInfo(uninstallerName()).path() + + QLatin1String("/../Info.plist"))); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + verbose() << "Checking for qt_menu.nib" << std::endl; + QString sourceDirName = QCoreApplication::applicationDirPath() + + QLatin1String("/../Resources/qt_menu.nib"); + if (QFileInfo(sourceDirName).exists()) { + verbose() << "qt_menu.nib has been found. Isn't it great?" << std::endl; + QString targetDirName = QFileInfo(QFileInfo(uninstallerName()).path() + + QLatin1String("/../Resources/qt_menu.nib")).absoluteFilePath(); + + // IFW has been built with a static Cocoa Qt. The app bundle must contain the qt_menu.nib. + // ### use the CopyDirectory operation in 1.1 + op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << targetDirName); + if (!op->performOperation()) { + verbose() << "ERROR in Mkdir operation: " << op->errorString() << std::endl; + } + + QDir sourceDir(sourceDirName); + foreach (const QString &filename, sourceDir.entryList(QDir::Files)) { + QString src = sourceDirName + QLatin1String("/") + filename; + QString dst = targetDirName + QLatin1String("/") + filename; + op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << src << dst); + if (!op->performOperation()) + verbose() << "ERROR in Copy operation: copy " << src << " to " << dst << std::endl + << "error message: " << op->errorString() << std::endl; + } + } + + // patch the Info.plist while copying it + QFile sourcePlist(QCoreApplication::applicationDirPath() + QLatin1String("/../Info.plist")); + openForRead(&sourcePlist, sourcePlist.fileName()); + QFile targetPlist(QFileInfo(uninstallerName()).path() + QLatin1String("/../Info.plist")); + openForWrite(&targetPlist, targetPlist.fileName()); + + QTextStream in(&sourcePlist); + QTextStream out(&targetPlist); + + while (!in.atEnd()) + { + QString line = in.readLine(); + line = line.replace(QLatin1String("<string>") + + QFileInfo(QCoreApplication::applicationFilePath()).baseName() + + QLatin1String("</string>"), QLatin1String("<string>") + + QFileInfo(uninstallerName()).baseName() + QLatin1String("</string>")); + out << line << endl; + } + + op = createOwnedOperation(QLatin1String("Mkdir")); + op->setArguments(QStringList() << (QFileInfo(QFileInfo(uninstallerName()).path()).path() + + QLatin1String("/Resources"))); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + const QString icon = QFileInfo(QCoreApplication::applicationFilePath()).baseName() + + QLatin1String(".icns"); + op = createOwnedOperation(QLatin1String("Copy")); + op->setArguments(QStringList() << (QCoreApplication::applicationDirPath() + + QLatin1String("/../Resources/") + icon) << (QFileInfo(uninstallerName()).path() + + QLatin1String("/../Resources/") + icon)); + performOperationThreaded(op, Backup); + performOperationThreaded(op); + + // finally, copy everything within Frameworks and plugins + if (QDir(QCoreApplication::applicationDirPath() + QLatin1String("/../Frameworks")).exists()) { + copyDirectoryContents(QCoreApplication::applicationDirPath() + + QLatin1String("/../Frameworks"), QFileInfo(uninstallerName()).path() + + QLatin1String("/../Frameworks")); + } + + if (QDir(QCoreApplication::applicationDirPath() + QLatin1String("/../plugins")).exists()) { + copyDirectoryContents(QCoreApplication::applicationDirPath() + + QLatin1String("/../plugins"), QFileInfo(uninstallerName()).path() + + QLatin1String("/../plugins")); + } + } +#endif + + QFile in; + if (isInstaller() || isUninstaller() || isPackageManager()) + in.setFileName(installerBinaryPath()); + else + in.setFileName(uninstallerName()); // we're the updater + + const QString installerBaseBinary = q->replaceVariables(installerBaseBinaryUnreplaced); + if (!installerBaseBinary.isEmpty()) + verbose() << "Got a replacement installer base binary: " << installerBaseBinary << std::endl; + + const bool haveSeparateExec = QFile::exists(installerBaseBinary) && !installerBaseBinary.isEmpty(); + verbose() << "Need to restart after exit: " << haveSeparateExec << " " + << qPrintable(installerBaseBinary) << std::endl; + +#ifdef Q_WS_WIN + KDSaveFile out(uninstallerName() + QLatin1String(".org")); +#else + KDSaveFile out(uninstallerName()); +#endif + + try { + ifVerbose("CREATING UNINSTALLER " << performedOperations.size()); + + QFile* execIn = ∈ + qint64 execSize = 0; + QFile ibbIn; + if (haveSeparateExec) { + ibbIn.setFileName(installerBaseBinary); + openForRead(&ibbIn, ibbIn.fileName()); + execIn = &ibbIn; + execSize = ibbIn.size(); + } + + openForRead(&in, in.fileName()); + openForWrite(&out, out.fileName()); + + const qint64 magicCookiePos = findMagicCookie(&in); + if (magicCookiePos < 0) { + throw Error(QObject::tr("Can not find the magic cookie in file %1. Are you sure this " + "is a valid installer?").arg(installerBinaryPath())); + } + + if (!in.seek(magicCookiePos - 7 * sizeof(qint64))) { + throw Error(QObject::tr("Failed to seek in file %1: %2").arg(installerBinaryPath(), + in.errorString())); + } + + qint64 resourceStart = retrieveInt64(&in); + const qint64 resourceLength = retrieveInt64(&in); + Q_ASSERT(resourceLength >= 0); + const qint64 _operationsStart = retrieveInt64(&in); + Q_UNUSED(_operationsStart); + Q_ASSERT(_operationsStart >= 0); + const qint64 _operationsLength = retrieveInt64(&in); + Q_UNUSED(_operationsLength); + Q_ASSERT(_operationsLength >= 0); + const qint64 count = retrieveInt64(&in); // atm always "1" + Q_ASSERT(count == 1); // we have just 1 resource atm + const qint64 dataBlockSize = retrieveInt64(&in); + const qint64 dataBlockStart = magicCookiePos + sizeof(qint64) - dataBlockSize; + resourceStart += dataBlockStart; + if (!haveSeparateExec) + execSize = dataBlockStart; + + // consider size difference between old and new installerbase executable (if updated) + const qint64 newResourceStart = execSize; + const qint64 magicmarker = retrieveInt64(&in); + Q_UNUSED(magicmarker); + Q_ASSERT(magicmarker == MagicInstallerMarker || magicmarker == MagicUninstallerMarker); + + + + if (!execIn->seek(0)) { + throw Error(QObject::tr("Failed to seek in file %1: %2").arg(execIn->fileName(), + execIn->errorString())); + } + appendData(&out, execIn, execSize); + + // copy the first few bytes to take the executable+resources over to the uninstaller. + if (! in.seek(resourceStart)) { + throw Error(QObject::tr("Failed to seek in file %1: %2").arg(in.fileName(), + in.errorString())); + } + Q_ASSERT(in.pos() == resourceStart && out.pos() == execSize); + + appendData(&out, &in, resourceLength); + const qint64 uninstallerDataBlockStart = out.pos(); + // compared to the installer we do not have component data but details about + // the performed operations during the installation to allow to undo them. + const qint64 operationsStart = out.pos(); + appendInt64(&out, performedOperations.count()); + foreach (KDUpdater::UpdateOperation *op, performedOperations) { + // the installer can't be put into XML, remove it first + op->clearValue(QLatin1String("installer")); + + appendString(&out, op->name()); + appendString(&out, op->toXml().toString()); + + // for the ui not to get blocked + qApp->processEvents(); + } + appendInt64(&out, performedOperations.count()); + const qint64 operationsEnd = out.pos(); + + // we dont save any component-indexes. + const qint64 numComponents = 0; + appendInt64(&out, numComponents); // for the indexes + // we dont save any components. + const qint64 compIndexStart = out.pos(); + appendInt64(&out, numComponents); // and 2 times number of components, + appendInt64(&out, numComponents); // one before and one after the components + const qint64 compIndexEnd = out.pos(); + + appendInt64Range(&out, Range<qint64>::fromStartAndEnd(compIndexStart, compIndexEnd) + .moved(-uninstallerDataBlockStart)); + appendInt64Range(&out, Range<qint64>::fromStartAndLength(newResourceStart, resourceLength) + .moved(-uninstallerDataBlockStart)); + appendInt64Range(&out, Range<qint64>::fromStartAndEnd(operationsStart, operationsEnd) + .moved(-uninstallerDataBlockStart)); + appendInt64(&out, count); + //data block size, from end of .exe to end of file + appendInt64(&out, out.pos() + 3 * sizeof(qint64) - uninstallerDataBlockStart); + appendInt64(&out, MagicUninstallerMarker); + appendInt64(&out, 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(uninstallerName(), + out.errorString())); + } + + //delete the installerbase binary temporarily installed for the uninstaller update + if (haveSeparateExec) { + QFile tmp(installerBaseBinary); + // Is there anything more sensible we can do with this error? I think not. + // It's not serious enough for throwing/aborting. + if (!tmp.remove()) { + verbose() << "Could not remove installerbase binary (" << installerBaseBinary + << ") after updating the uninstaller: " << tmp.errorString() << std::endl; + } + installerBaseBinaryUnreplaced.clear(); + } + +#ifdef Q_WS_WIN + deferredRename(out.fileName(), QFileInfo(uninstallerName()).fileName(), + haveSeparateExec && !isInstaller()); +#else + verbose() << " preparing restart " << std::endl; + if (haveSeparateExec && !isInstaller()) + KDSelfRestarter::setRestartOnQuit(true); +#endif + } catch (const Error &err) { + setStatus(InstallerFailed); + if (gainedAdminRights) + q->dropAdminRights(); + m_needToWriteUninstaller = false; + throw err; + } + + if (gainedAdminRights) + q->dropAdminRights(); + + commitSessionOperations(); + + m_needToWriteUninstaller = false; +} + +QString Installer::Private::registerPath() const +{ + 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")) == QLatin1String("true")) + path = QLatin1String("HKEY_LOCAL_MACHINE"); + + return path + QLatin1String("\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\") + + productName; +} + +void Installer::Private::registerInstaller() +{ +#ifdef Q_OS_WIN + QSettings settings(registerPath(), QSettings::NativeFormat); + settings.setValue(QLatin1String("DisplayName"), 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(QLatin1String("Publisher"), m_vars.value(QLatin1String("Publisher"))); + settings.setValue(QLatin1String("UrlInfoAbout"), m_vars.value(QLatin1String("Url"))); + settings.setValue(QLatin1String("Comments"), m_vars.value(QLatin1String("Title"))); + 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"), 1); // TODO: set to 0 and support modify + settings.setValue(QLatin1String("NoRepair"), 1); +#endif +} + +void Installer::Private::unregisterInstaller() +{ +#ifdef Q_OS_WIN + QSettings settings(registerPath(), QSettings::NativeFormat); + settings.remove(QString()); +#endif +} + +void Installer::autoAcceptMessageBoxes() +{ + MessageBoxHandler::instance()->setDefaultAction(MessageBoxHandler::Accept); +} + +void Installer::autoRejectMessageBoxes() +{ + MessageBoxHandler::instance()->setDefaultAction(MessageBoxHandler::Reject); +} + +void Installer::setMessageBoxAutomaticAnswer(const QString &identifier, int button) +{ + MessageBoxHandler::instance()->setAutomaticAnswer(identifier, + static_cast<QMessageBox::Button>(button)); +} + +void Installer::installSelectedComponents() +{ + d->setStatus(InstallerRunning); + // download + + double downloadPartProgressSize = double(1)/3; + double componentsInstallPartProgressSize = double(2)/3; + // get the list of packages we need to install in proper order and do it for the updater + downloadNeededArchives(UpdaterMode, downloadPartProgressSize); + // get the list of packages we need to install in proper order + const QList<Component*> components = calculateComponentOrder(UpdaterMode); + + if (!isInstaller() && !QFileInfo(installerBinaryPath()).isWritable()) + gainAdminRights(); + + d->stopProcessesForUpdates(components); + int progressOperationCount = d->countProgressOperations(components); + double progressOperationSize = componentsInstallPartProgressSize / progressOperationCount; + + //TODO: devide this in undo steps and install steps (2 "for" loops) for better progress calculation + for (QList<Component*>::const_iterator it = components.begin(); it != components.end(); ++it) { + if (d->statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + Component* const currentComponent = *it; + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nRemoving the old " + "version of: %1").arg(currentComponent->name())); + if (isUpdater() && currentComponent->removeBeforeUpdate()) { + // undo all operations done by this component upon installation + for (int i = d->m_performedOperationsOld.count() - 1; i >= 0; --i) { + KDUpdater::UpdateOperation* const op = d->m_performedOperationsOld[i]; + if (op->value(QLatin1String("component")) != currentComponent->name()) + continue; + const bool becameAdmin = !d->engineClientHandler->isActive() + && op->value(QLatin1String("admin")).toBool() && gainAdminRights(); + performOperationThreaded(op, Undo); + if (becameAdmin) + dropAdminRights(); + d->m_performedOperationsOld.remove(i); + delete op; + } + d->m_app->packagesInfo()->removePackage(currentComponent->name()); + d->m_app->packagesInfo()->writeToDisk(); + } + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged( + tr("\nInstalling the new version of: %1").arg(currentComponent->name())); + installComponent(currentComponent, progressOperationSize); + //commit all operations for this allready updated/installed component + //so an undo during the installComponent function only undos the uncomplete installed one + d->commitSessionOperations(); + d->m_needToWriteUninstaller = true; + } + + d->setStatus(InstallerSucceeded); + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nUpdate finished!")); + emit updateFinished(); +} + +quint64 Installer::requiredDiskSpace() const +{ + quint64 result = 0; + QList<Component*>::const_iterator it; + const QList<Component*> availableComponents = components(true); + for (it = availableComponents.begin(); it != availableComponents.end(); ++it) { + Component* const comp = *it; + if (!comp->isSelected()) + continue; + if (comp->value(QLatin1String("PreviousState")) == QLatin1String("Installed")) + continue; + result += comp->value(QLatin1String("UncompressedSize")).toLongLong(); + } + return result; +} + +quint64 Installer::requiredTemporaryDiskSpace() const +{ + quint64 result = 0; + QList<Component*>::const_iterator it; + const QList<Component*> availableComponents = components(true); + for (it = availableComponents.begin(); it != availableComponents.end(); ++it) { + Component* const comp = *it; + if (!comp->isSelected()) + continue; + if (comp->value(QLatin1String("PreviousState")) == QLatin1String("Installed")) + continue; + result += comp->value(QLatin1String("CompressedSize")).toLongLong(); + } + return result; +} + +/*! + Returns the will be downloaded archives count +*/ +int Installer::downloadNeededArchives(RunModes runMode, double partProgressSize) +{ + Q_ASSERT(partProgressSize >= 0 && partProgressSize <= 1); + + QList<Component*> neededComponents; + QList<QPair<QString, QString> > archivesToDownload; + + QList<Component*>::const_iterator it; + const QList<Component*> availableComponents = components(true, runMode); + for (it = availableComponents.begin(); it != availableComponents.end(); ++it) { + Component* const comp = *it; + if (!comp->isSelected(runMode)) + continue; + if (comp->value(QLatin1String("PreviousState")) == QLatin1String("Installed") + && runMode == InstallerMode) { + continue; + } + appendComponentAndMissingDependencies(neededComponents, comp); + } + + for (it = neededComponents.begin(); it != neededComponents.end(); ++it) { + Component* const comp = *it; + + // collect all archives to be downloaded + const QStringList toDownload = comp->downloadableArchives(); + for (QStringList::const_iterator it2 = toDownload.begin(); it2 != toDownload.end(); ++it2) { + QString versionFreeString(*it2); + archivesToDownload.push_back(qMakePair(QString::fromLatin1("installer://%1/%2") + .arg(comp->name(), versionFreeString), QString::fromLatin1("%1/%2/%3") + .arg(comp->repositoryUrl().toString(), comp->name(), versionFreeString))); + } + } + + if (archivesToDownload.isEmpty()) + return 0; + + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nDownloading packages...")); + + // don't have it on the stack, since it keeps the temporary files + DownloadArchivesJob* const archivesJob = new DownloadArchivesJob(d->m_settings.publicKey(), this); + archivesJob->setArchivesToDownload(archivesToDownload); + archivesJob->setAutoDelete(false); + connect(archivesJob, SIGNAL(outputTextChanged(QString)), ProgressCoordninator::instance(), + SLOT(emitLabelAndDetailTextChanged(QString))); + ProgressCoordninator::instance()->registerPartProgress(archivesJob, + SIGNAL(progressChanged(double)), partProgressSize); + connect(this, SIGNAL(installationInterrupted()), archivesJob, SLOT(cancel())); + archivesJob->start(); + archivesJob->waitForFinished(); + + if (archivesJob->error() == KDJob::Canceled) + interrupt(); + else if (archivesJob->error() != DownloadArchivesJob::NoError) + throw Error(archivesJob->errorString()); + if (d->statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + + return archivesToDownload.count(); +} + +QList<Component*> Installer::calculateComponentOrder(RunModes runMode) const +{ + return componentsToInstall(true, true, runMode); +} + +void Installer::installComponent(Component* comp, double progressOperationSize) +{ + Q_ASSERT(progressOperationSize); + + d->setStatus(InstallerRunning); + const QList<KDUpdater::UpdateOperation*> operations = comp->operations(); + + // show only component which are doing something, MinimumProgress is only for progress + // calculation safeness + if (operations.count() > 1 + || (operations.count() == 1 && operations.at(0)->name() != QLatin1String("MinimumProgress"))) { + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nInstalling component %1") + .arg(comp->displayName())); + } + + if (!comp->operationsCreatedSuccessfully()) + setCanceled(); + + QList<KDUpdater::UpdateOperation*>::const_iterator op; + for (op = operations.begin(); op != operations.end(); ++op) { + if (d->statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + + KDUpdater::UpdateOperation* const operation = *op; + d->connectOperationToInstaller(operation, progressOperationSize); + + // maybe this operations wants us to be admin... + const bool becameAdmin = !d->engineClientHandler->isActive() + && operation->value(QLatin1String("admin")).toBool() && gainAdminRights(); + // perform the operation + if (becameAdmin) + verbose() << operation->name() << " as admin: " << becameAdmin << std::endl; + + // allow the operation to backup stuff before performing the operation + performOperationThreaded(operation, Backup); + + bool ignoreError = false; + bool ok = performOperationThreaded(operation); + while (!ok && !ignoreError && status() != InstallerCanceledByUser) { + verbose() << QString(QLatin1String("operation '%1' with arguments: '%2' failed: %3")) + .arg(operation->name(), operation->arguments().join(QLatin1String("; ")), + operation->errorString()) << std::endl;; + const QMessageBox::StandardButton button = + MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationErrorWithRetry"), tr("Installer Error"), + tr("Error during installation process:\n%1").arg(operation->errorString()), + QMessageBox::Retry | QMessageBox::Ignore | QMessageBox::Cancel, QMessageBox::Retry); + + if (button == QMessageBox::Retry) + ok = performOperationThreaded(operation); + else if (button == QMessageBox::Ignore) + ignoreError = true; + else if (button == QMessageBox::Cancel) + interrupt(); + } + + if (ok || operation->error() > KDUpdater::UpdateOperation::InvalidArguments) { + // remember that the operation was performed what allows us to undo it if a + // following operaton fails or if this operation failed but still needs + // an undo call to cleanup. + d->addPerformed(operation); + operation->setValue(QLatin1String("component"), comp->name()); + } + + if (becameAdmin) + dropAdminRights(); + + if (!ok && !ignoreError) + throw Error(operation->errorString()); + + if (comp->value(QLatin1String("Important"), QLatin1String("false")) == QLatin1String("true")) + d->m_forceRestart = true; + } + + d->registerPathesForUninstallation(comp->pathesForUninstallation(), comp->name()); + + if (!comp->stopProcessForUpdateRequests().isEmpty()) { + KDUpdater::UpdateOperation *stopProcessForUpdatesOp = + KDUpdater::UpdateOperationFactory::instance().create(QLatin1String("FakeStopProcessForUpdate")); + const QStringList arguments(comp->stopProcessForUpdateRequests().join(QLatin1String(","))); + stopProcessForUpdatesOp->setArguments(arguments); + d->addPerformed(stopProcessForUpdatesOp); + stopProcessForUpdatesOp->setValue(QLatin1String("component"), comp->name()); + } + + // now mark the component as installed + KDUpdater::PackagesInfo* const packages = d->m_app->packagesInfo(); + const bool forcedInstall = + comp->value(QLatin1String("ForcedInstallation")).toLower() == QLatin1String("true") + ? true : false; + const bool virtualComponent = + comp->value(QLatin1String ("Virtual")).toLower() == QLatin1String("true") ? true : false; + packages->installPackage(comp->value(QLatin1String("Name")), + comp->value(QLatin1String("Version")), comp->value(QLatin1String("DisplayName")), + comp->value(QLatin1String("Description")), comp->dependencies(), forcedInstall, + virtualComponent, comp->value(QLatin1String ("UncompressedSize")).toULongLong()); + + comp->setValue(QLatin1String("CurrentState"), QLatin1String("Installed")); + comp->markAsPerformedInstallation(); +} + +/*! + If a component marked as important was installed during update + process true is returned. +*/ +bool Installer::needsRestart() const +{ + return d->m_forceRestart; +} + +void Installer::rollBackInstallation() +{ + emit titleMessageChanged(tr("Cancelling the Installer")); + // rolling back + + //this unregisters all operation progressChanged connects + ProgressCoordninator::instance()->setUndoMode(); + int progressOperationCount = + d->countProgressOperations(d->m_performedOperationsCurrentSession.toList()); + double progressOperationSize = double(1) / progressOperationCount; + + //reregister all the undooperations with the new size to the ProgressCoordninator + foreach (KDUpdater::UpdateOperation* const operation, d->m_performedOperationsCurrentSession) { + QObject* const operationObject = dynamic_cast<QObject*>(operation); + if (operationObject != 0) { + const QMetaObject* const mo = operationObject->metaObject(); + if (mo->indexOfSignal(QMetaObject::normalizedSignature("progressChanged(double)")) > -1) { + ProgressCoordninator::instance()->registerPartProgress(operationObject, + SIGNAL(progressChanged(double)), progressOperationSize); + } + } + } + + while (!d->m_performedOperationsCurrentSession.isEmpty()) { + try { + KDUpdater::UpdateOperation* const operation = d->m_performedOperationsCurrentSession.last(); + d->m_performedOperationsCurrentSession.pop_back(); + + const bool becameAdmin = !d->engineClientHandler->isActive() + && operation->value(QLatin1String("admin")).toBool() && gainAdminRights(); + performOperationThreaded(operation, Undo); + if (becameAdmin) + dropAdminRights(); + } catch(const Error &e) { + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("ElevationError"), tr("Authentication Error"), tr("Some components " + "could not be removed completely because admin rights could not be acquired: %1.") + .arg(e.message())); + } + } +} + +void Installer::Private::runInstaller() +{ + try { + setStatus(InstallerRunning); + emit installationStarted(); //resets also the ProgressCoordninator + + //to have some progress for writeUninstaller + ProgressCoordninator::instance()->addReservePercentagePoints(1); + + const QString target = targetDir(); + if (target.isEmpty()) + throw Error(tr("Variable 'TargetDir' not set.")); + + // add the operation to create the target directory + bool installToAdminDirectory = false; + if (!QDir(target).exists()) { + QScopedPointer<KDUpdater::UpdateOperation> mkdirOp(createOwnedOperation(QLatin1String("Mkdir"))); + mkdirOp->setValue(QLatin1String("forceremoval"), true); + Q_ASSERT(mkdirOp.data()); + mkdirOp->setArguments(QStringList() << target); + performOperationThreaded(mkdirOp.data(), Backup); + if (!performOperationThreaded(mkdirOp.data())) { + // if we cannot create the target dir, we try to activate the admin rights + installToAdminDirectory = true; + if (!q->gainAdminRights() || !performOperationThreaded(mkdirOp.data())) + throw Error(mkdirOp->errorString()); + } + addPerformed(mkdirOp.take()); + } else { + QTemporaryFile tempAdminFile(target + QLatin1String("/adminrights")); + if (!tempAdminFile.open() || !tempAdminFile.isWritable()) + installToAdminDirectory = q->gainAdminRights(); + } + + // to show that there was some work + ProgressCoordninator::instance()->addManualPercentagePoints(1); + + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("Preparing the installation...")); + const QList<Component*> componentsToInstall = q->calculateComponentOrder(); + verbose() << "Install size: " << componentsToInstall.size() << " components" << std::endl; + + // check if we need admin rights and ask before the action happens + for (QList<Component*>::const_iterator it = componentsToInstall.begin(); + it != componentsToInstall.end() && !installToAdminDirectory; ++it) { + Component* const component = *it; + bool requiredAdmin = false; + + if (component->value(QLatin1String("RequiresAdminRights"), + QLatin1String("false")) == QLatin1String("true")) { + requiredAdmin = q->gainAdminRights(); + } + + if (requiredAdmin) { + q->dropAdminRights(); + break; + } + } + + const double downloadPartProgressSize = double(1) / 3; + double componentsInstallPartProgressSize = double(2) / 3; + const int downloadedArchivesCount = q->downloadNeededArchives(InstallerMode, + downloadPartProgressSize); + + //if there was no download we have the whole progress for installing components + if (!downloadedArchivesCount) { + //componentsInstallPartProgressSize + downloadPartProgressSize; + componentsInstallPartProgressSize = double(1); + } + + // put the installed packages info into the target dir + KDUpdater::PackagesInfo* const packages = m_app->packagesInfo(); + packages->setFileName(componentsXmlPath()); + packages->setApplicationName(m_settings.applicationName()); + packages->setApplicationVersion(m_settings.applicationVersion()); + + stopProcessesForUpdates(componentsToInstall); + + const int progressOperationCount = countProgressOperations(componentsToInstall); + double progressOperationSize = componentsInstallPartProgressSize / progressOperationCount; + + QList<Component*>::const_iterator it; + for (it = componentsToInstall.begin(); it != componentsToInstall.end(); ++it) + q->installComponent(*it, progressOperationSize); + + registerInstaller(); + + emit q->titleMessageChanged(tr("Creating Uninstaller")); + + m_app->packagesInfo()->writeToDisk(); + writeUninstaller(m_performedOperationsOld + m_performedOperationsCurrentSession); + + //this is the reserved one from the beginning + ProgressCoordninator::instance()->addManualPercentagePoints(1); + + setStatus(InstallerSucceeded); + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nInstallation finished!")); + + emit installationFinished(); + + // disable the FSEngineClientHandler afterwards + engineClientHandler->setActive(false); + } catch (const Error &err) { + if (q->status() != InstallerCanceledByUser) { + setStatus(InstallerFailed); + verbose() << "INSTALLER FAILED: " << err.message() << std::endl; + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + verbose() << "ROLLING BACK operations=" << m_performedOperationsCurrentSession.count() + << std::endl; + } + + q->rollBackInstallation(); + + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("Installation aborted")); + emit installationFinished(); + + // disable the FSEngineClientHandler afterwards + engineClientHandler->setActive(false); + throw; + } +} + +void Installer::Private::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 outself. + // 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 KDUpdater::UpdateOperation* const op = m_performedOperationsOld.first(); + if (op->name() == QLatin1String("Mkdir")) // the target directory name + arguments << QDir::toNativeSeparators(QFileInfo(op->arguments().first()).absoluteFilePath()); + } + + if (!QProcess::startDetached(QLatin1String("cscript"), arguments, QDir::rootPath())) + throw Error(tr("Cannot start uninstall")); +#else + // every other platform has no problem if we just delete ourself 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("/components.xml")); + } + else +#endif +#endif + { + // finally remove the components.xml, since it still exists now + QFile::remove(QFileInfo(installerBinaryPath()).absolutePath() + QLatin1String("/components.xml")); + } +} + +bool Installer::isFileExtensionRegistered(const QString& extension) const +{ + QSettings settings(QLatin1String("HKEY_CLASSES_ROOT"), QSettings::NativeFormat); + return settings.value(QString::fromLatin1(".%1/Default").arg(extension)).isValid(); +} + +void Installer::Private::runPackageUpdater() +{ + try { + if (m_completeUninstall) { + // well... I guess we would call that an uninstall, no? :-) + packageManagingMode = !m_completeUninstall; + runUninstaller(); + return; + } + + setStatus(InstallerRunning); + emit installationStarted(); //resets also the ProgressCoordninator + + //to have some progress for the cleanup/write component.xml step + ProgressCoordninator::instance()->addReservePercentagePoints(1); + + if (!QFileInfo(installerBinaryPath()).isWritable()) + q->gainAdminRights(); + + KDUpdater::PackagesInfo* const packages = m_app->packagesInfo(); + packages->setFileName(componentsXmlPath()); + packages->setApplicationName(m_settings.applicationName()); + packages->setApplicationVersion(m_settings.applicationVersion()); + + const QString packagesXml = componentsXmlPath(); + if (!QFile(packagesXml).open(QIODevice::Append)) + q->gainAdminRights(); + + // first check, if we need admin rights for the installation part + QList<Component*>::const_iterator it; + QList<Component*> availableComponents = q->components(true, InstallerMode); + for (it = availableComponents.begin(); it != availableComponents.end(); ++it) { + // check if we need admin rights and ask before the action happens + Component* const currentComponent = *it; + if (!currentComponent->isSelected(InstallerMode)) + continue; + + // we only need the uninstalled components + if (currentComponent->value(QLatin1String("PreviousState")) == QLatin1String("Installed")) + continue; + + bool requiredAdmin = false; + if (currentComponent->value(QLatin1String("RequiresAdminRights"), + QLatin1String("false")) == QLatin1String("true")) { + requiredAdmin = q->gainAdminRights(); + } + + if (requiredAdmin) { + q->dropAdminRights(); + break; + } + } + + //to have 1/5 for undoOperationProgressSize and 2/5 for componentsInstallPartProgressSize + const double downloadPartProgressSize = double(2) / 5; + // following, we download the needed archives + q->downloadNeededArchives(InstallerMode, downloadPartProgressSize); + + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("Removing deselected components...")); + QVector< KDUpdater::UpdateOperation* > nonRevertedOperations; + + QList<KDUpdater::UpdateOperation*> undoOperations; + for (int i = m_performedOperationsOld.count() - 1; i >= 0; --i) { + KDUpdater::UpdateOperation* const currentOperation = m_performedOperationsOld[i]; + + const QString componentName = currentOperation->value(QLatin1String("component")).toString(); + Component* comp = q->componentByName(componentName); + + // if we're _not_ removing everything an this component is still selected, -> next + if (comp == 0 || comp->isSelected()) { + nonRevertedOperations.push_front(currentOperation); + continue; + } + undoOperations.append(currentOperation); + } + + double undoOperationProgressSize = 0; + int progressUndoOperationCount = 0; + double progressUndoOperationSize = 0; + double componentsInstallPartProgressSize = double(2) / 5; + if (undoOperations.count() > 0) { + componentsInstallPartProgressSize = double(2) / 5; + undoOperationProgressSize = double(1) / 5; + progressUndoOperationCount = countProgressOperations(undoOperations); + progressUndoOperationSize = undoOperationProgressSize / progressUndoOperationCount; + } else { + componentsInstallPartProgressSize = double(3) / 5; + } + + QSet<Component*> uninstalledComponents; + foreach (KDUpdater::UpdateOperation* const currentOperation, undoOperations) { + if (statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + connectOperationToInstaller(currentOperation, progressUndoOperationSize); + + const QString componentName = currentOperation->value(QLatin1String("component")).toString(); + Component* comp = q->componentByName(componentName); + + verbose() << "undo operation=" << currentOperation->name() << std::endl; + uninstalledComponents |= comp; + + const bool becameAdmin = !engineClientHandler->isActive() + && currentOperation->value(QLatin1String("admin")).toBool() && q->gainAdminRights(); + + bool ignoreError = false; + performOperationThreaded(currentOperation, Undo); + bool ok = currentOperation->error() == KDUpdater::UpdateOperation::NoError + || componentName == QLatin1String(""); + while (!ok && !ignoreError && q->status() != InstallerCanceledByUser) { + verbose() << QString(QLatin1String("operation '%1' with arguments: '%2' failed: %3")) + .arg(currentOperation->name(), currentOperation->arguments() + .join(QLatin1String("; ")), currentOperation->errorString()) << std::endl;; + + const QMessageBox::StandardButton button = + MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationErrorWithRetry"), tr("Installer Error"), + tr("Error during installation process:\n%1").arg(currentOperation->errorString()), + QMessageBox::Retry | QMessageBox::Ignore, QMessageBox::Retry); + + if (button == QMessageBox::Retry) { + performOperationThreaded(currentOperation, Undo); + ok = currentOperation->error() == KDUpdater::UpdateOperation::NoError; + } + else if (button == QMessageBox::Ignore) + ignoreError = true; + } + + if (becameAdmin) + q->dropAdminRights(); + + delete currentOperation; + } + + const QList<Component*> allComponents = q->components(true, InstallerMode); + foreach (Component *comp, allComponents) { + if (!comp->isSelected()) + uninstalledComponents |= comp; + } + + QSet<Component*>::const_iterator it2; + for (it2 = uninstalledComponents.begin(); it2 != uninstalledComponents.end(); ++it2) { + packages->removePackage((*it2)->name()); + (*it2)->setValue(QLatin1String("CurrentState"), QLatin1String("Uninstalled")); + } + + // these are all operations left: those which were not reverted + m_performedOperationsOld = nonRevertedOperations; + + //write components.xml in case the user cancels the update + packages->writeToDisk(); + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("Preparing the installation...")); + + const QList<Component*> componentsToInstall = q->calculateComponentOrder(); + + verbose() << "Install size: " << componentsToInstall.size() << " components " << std::endl; + + stopProcessesForUpdates(componentsToInstall); + + int progressOperationCount = countProgressOperations(componentsToInstall); + double progressOperationSize = componentsInstallPartProgressSize / progressOperationCount; + + for (it = componentsToInstall.begin(); it != componentsToInstall.end(); ++it) + q->installComponent(*it, progressOperationSize); + + packages->writeToDisk(); + + emit q->titleMessageChanged(tr("Creating Uninstaller")); + + commitSessionOperations(); //end session, move ops to "old" + m_needToWriteUninstaller = true; + + //this is the reserved one from the beginning + ProgressCoordninator::instance()->addManualPercentagePoints(1); + + setStatus(InstallerSucceeded); + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nInstallation finished!")); + emit installationFinished(); + + // disable the FSEngineClientHandler afterwards + engineClientHandler->setActive(false); + } catch(const Error &err) { + if (q->status() != InstallerCanceledByUser) { + setStatus(InstallerFailed); + verbose() << "INSTALLER FAILED: " << err.message() << std::endl; + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + verbose() << "ROLLING BACK operations=" << m_performedOperationsCurrentSession.count() + << std::endl; + } + + q->rollBackInstallation(); + + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("Installation aborted")); + emit installationFinished(); + + // disable the FSEngineClientHandler afterwards + engineClientHandler->setActive(false); + throw; + } +} + +void Installer::Private::runUninstaller() +{ + try { + emit uninstallationStarted(); + + if (!QFileInfo(installerBinaryPath()).isWritable()) + q->gainAdminRights(); + + const QString packagesXml = componentsXmlPath(); + if (!QFile(packagesXml).open(QIODevice::Append)) + q->gainAdminRights(); + + KDUpdater::PackagesInfo* const packages = m_app->packagesInfo(); + packages->setFileName(componentsXmlPath()); + packages->setApplicationName(m_settings.applicationName()); + packages->setApplicationVersion(m_settings.applicationVersion()); + + // iterate over all components - if they're all marked for uninstall, it's a complete uninstall + const QList<Component*> allComponents = q->components(true); + bool allMarkedForUninstall = true; + + QList<KDUpdater::UpdateOperation*> uninstallOperations; + QVector<KDUpdater::UpdateOperation*> nonRevertedOperations; + + // just rollback all operations done before + for (int i = m_performedOperationsOld.count() - 1; i >= 0; --i) { + KDUpdater::UpdateOperation* const operation = m_performedOperationsOld[i]; + + const QString componentName = operation->value(QLatin1String("component")).toString(); + const Component* const comp = q->componentByName(componentName); + + // if we're _not_ removing everything an this component is still selected, -> next + if (!m_completeUninstall && (comp == 0 || !comp->isSelected())) { + nonRevertedOperations.push_front(operation); + continue; + } + uninstallOperations.append(operation); + } + + const int progressUninstallOperationCount = countProgressOperations(uninstallOperations); + const double progressUninstallOperationSize = double(1) / progressUninstallOperationCount; + + foreach (KDUpdater::UpdateOperation* const currentOperation, uninstallOperations) { + if (statusCanceledOrFailed()) + throw Error(tr("Installation canceled by user")); + + connectOperationToInstaller(currentOperation, progressUninstallOperationSize); + verbose() << "undo operation=" << currentOperation->name() << std::endl; + + const QString componentName = currentOperation->value(QLatin1String("component")).toString(); + + const bool becameAdmin = !engineClientHandler->isActive() + && currentOperation->value(QLatin1String("admin")).toBool() && q->gainAdminRights(); + + bool ignoreError = false; + performOperationThreaded(currentOperation, Undo); + bool ok = currentOperation->error() == KDUpdater::UpdateOperation::NoError + || componentName == QLatin1String(""); + while (!ok && !ignoreError && q->status() != InstallerCanceledByUser) { + verbose() << QString(QLatin1String("operation '%1' with arguments: '%2' failed: %3")) + .arg(currentOperation->name(), currentOperation->arguments() + .join(QLatin1String("; ")), currentOperation->errorString()) << std::endl;; + const QMessageBox::StandardButton button = + MessageBoxHandler::warning(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationErrorWithRetry"), tr("Installer Error"), + tr("Error during installation process:\n%1").arg(currentOperation->errorString()), + QMessageBox::Retry | QMessageBox::Ignore, QMessageBox::Retry); + + if (button == QMessageBox::Retry) { + performOperationThreaded(currentOperation, Undo); + ok = currentOperation->error() == KDUpdater::UpdateOperation::NoError; + } else if (button == QMessageBox::Ignore) + ignoreError = true; + } + + if (becameAdmin) + q->dropAdminRights(); + + if (!m_completeUninstall) + delete currentOperation; + } + + if (!m_completeUninstall) { + QList<Component*>::const_iterator it; + for (it = allComponents.begin(); it != allComponents.end(); ++it) { + Component* const comp = *it; + if (comp->isSelected()) { + allMarkedForUninstall = false; + } else { + packages->removePackage(comp->name()); + comp->setValue(QLatin1String("CurrentState"), QLatin1String("Uninstalled")); + } + } + m_completeUninstall = m_completeUninstall || allMarkedForUninstall; + packageManagingMode = ! m_completeUninstall; + } + + const QString startMenuDir = m_vars.value(QLatin1String("StartMenuDir")); + if (!startMenuDir.isEmpty()) { + errno = 0; + if (!QDir().rmdir(startMenuDir)) { + verbose() << "Could not remove " << startMenuDir << " : " + << QLatin1String(strerror(errno)) << std::endl; + } else { + verbose() << "Startmenu dir not set" << std::endl; + } + } + + if (m_completeUninstall) { + // this will also delete the TargetDir on Windows + deleteUninstaller(); + QList<Component*>::const_iterator it; + for (it = allComponents.begin(); it != allComponents.end(); ++it) { + packages->removePackage((*it)->name()); + (*it)->setValue(QLatin1String("CurrentState"), QLatin1String("Uninstalled")); + } + + // on !Windows, we need to remove TargetDir manually + packageManagingMode = ! m_completeUninstall; + verbose() << "Complete Uninstallation is chosen" << std::endl; + const QString target = targetDir(); + if (!target.isEmpty()) { + if (engineClientHandler->isServerRunning() && !engineClientHandler->isActive()) { + // we were root at least once, so we remove the target dir as root + q->gainAdminRights(); + removeDirectoryThreaded(target, true); + q->dropAdminRights(); + } else { + removeDirectoryThreaded(target, true); + } + } + + unregisterInstaller(); + m_needToWriteUninstaller = false; + } else { + // rewrite the uninstaller with the operation we did not undo + writeUninstaller(nonRevertedOperations); + } + + setStatus(InstallerSucceeded); + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("\nDeinstallation finished")); + + engineClientHandler->setActive(false); + } catch (const Error &err) { + if (q->status() != InstallerCanceledByUser) { + setStatus(InstallerFailed); + verbose() << "INSTALLER FAILED: " << err.message() << std::endl; + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + verbose() << "ROLLING BACK operations=" << m_performedOperationsCurrentSession.count() + << std::endl; + } + + ProgressCoordninator::instance()->emitLabelAndDetailTextChanged(tr("Installation aborted")); + emit installationFinished(); + + // disable the FSEngineClientHandler afterwards + engineClientHandler->setActive(false); + throw; + } + emit uninstallationFinished(); +} + + +// -- QInstaller + + +Installer::Installer(qint64 magicmaker, + const QVector<KDUpdater::UpdateOperation*>& performedOperations) + : d(new Private(this, magicmaker, performedOperations)) +{ + qRegisterMetaType< QInstaller::Installer::Status >("QInstaller::Installer::Status"); + qRegisterMetaType< QInstaller::Installer::WizardPage >("QInstaller::Installer::WizardPage"); + + d->initialize(); +} + +Installer::~Installer() +{ + if (!isUninstaller() && !(isInstaller() && status() == InstallerCanceledByUser)) { + QDir targetDir(value(QLatin1String("TargetDir"))); + QString logFileName = targetDir.absoluteFilePath(value(QLatin1String("LogFileName"), + QLatin1String("InstallationLog.txt"))); + QInstaller::VerboseWriter::instance()->setOutputStream(logFileName); + } + + d->engineClientHandler->setActive(false); + delete d; +} + +/*! + Adds the widget with objectName() \a name registered by \a component as a new page + into the installer's GUI wizard. The widget is added before \a page. + \a page has to be a value of \ref QInstaller::Installer::WizardPage "WizardPage". +*/ +bool Installer::addWizardPage(Component* component, const QString &name, int page) +{ + if (QWidget* const widget = component->userInterface(name)) { + emit wizardPageInsertionRequested(widget, static_cast<WizardPage>(page)); + return true; + } + return false; +} + +/*! + Removes the widget with objectName() \a name previously added to the installer's wizard + by \a component. +*/ +bool Installer::removeWizardPage(Component *component, const QString &name) +{ + if (QWidget* const widget = component->userInterface(name)) { + emit wizardPageRemovalRequested(widget); + return true; + } + return false; +} + +/*! + Sets the visibility of the default page with id \a page to \a visible, i.e. + removes or adds it from/to the wizard. This works only for pages which have been + in the installer when it was started. + */ +bool Installer::setDefaultPageVisible(int page, bool visible) +{ + emit wizardPageVisibilityChangeRequested(visible, page); + return true; +} + +/*! + Adds the widget with objectName() \a name registered by \a component as an GUI element + into the installer's GUI wizard. The widget is added on \a page. + \a page has to be a value of \ref QInstaller::Installer::WizardPage "WizardPage". +*/ +bool Installer::addWizardPageItem(Component *component, const QString &name, int page) +{ + if (QWidget* const widget = component->userInterface(name)) { + emit wizardWidgetInsertionRequested(widget, static_cast<WizardPage>(page)); + return true; + } + return false; +} + +/*! + Removes the widget with objectName() \a name previously added to the installer's wizard + by \a component. +*/ +bool Installer::removeWizardPageItem(Component *component, const QString &name) +{ + if (QWidget* const widget = component->userInterface(name)) { + emit wizardWidgetRemovalRequested(widget); + return true; + } + return false; +} + +void Installer::setRemoteRepositories(const QList<Repository> &repositories) +{ + GetRepositoriesMetaInfoJob metaInfoJob(d->m_settings.publicKey(), isPackageManager()); + metaInfoJob.setRepositories(repositories); + + // start... + metaInfoJob.setAutoDelete(false); + metaInfoJob.start(); + metaInfoJob.waitForFinished(); + + if (metaInfoJob.isCanceled()) + return; + + if (metaInfoJob.error() != KDJob::NoError) + throw Error(tr("Could not retrieve updates: %1").arg(metaInfoJob.errorString())); + + KDUpdater::Application &updaterApp = *d->m_app; + + const QStringList tempDirs = metaInfoJob.temporaryDirectories(); + d->tempDirDeleter.add(tempDirs); + foreach (const QString &tmpDir, tempDirs) { + if (tmpDir.isEmpty()) + continue; + const QString &applicationName = d->m_settings.applicationName(); + updaterApp.addUpdateSource(applicationName, applicationName, QString(), + QUrl::fromLocalFile(tmpDir), 1); + } + + if (updaterApp.updateSourcesInfo()->updateSourceInfoCount() == 0) + throw Error(tr("Could not reach any update sources.")); + + if (!setAndParseLocalComponentsFile(*updaterApp.packagesInfo())) + return; + + // The changes done above by adding update source don't count as modification that + // need to be saved cause they will be re-set on the next start anyway. This does + // prevent creating of xml files not needed atm. We can still set the modified + // state later once real things changed that we like to restore at the next startup. + updaterApp.updateSourcesInfo()->setModified(false); + + // create the packages info + KDUpdater::UpdateFinder* const updateFinder = new KDUpdater::UpdateFinder(&updaterApp); + connect(updateFinder, SIGNAL(error(int, QString)), &updaterApp, SLOT(printError(int, QString))); + updateFinder->setUpdateType(KDUpdater::PackageUpdate | KDUpdater::NewPackage); + updateFinder->run(); + + // now create installable componets + createComponents(updateFinder->updates(), metaInfoJob); +} + +/*! + Sets additional repository for this instance of the installer or updater + Will be removed after invoking it again +*/ +void Installer::setTemporaryRepositories(const QList<Repository> &repositories, bool replace) +{ + d->m_settings.setTemporaryRepositories(repositories, replace); +} + +/*! + Defines if the downloader should try to download sha1 checksums for archives +*/ +void Installer::setTestChecksum(bool test) +{ + d->m_testChecksum = test; +} + +/*! + checks if the downloader should try to download sha1 checksums for archives +*/ +bool Installer::testChecksum() +{ + return d->m_testChecksum; +} + +/*! + Creates components from the \a updates found by KDUpdater. +*/ +void Installer::createComponents(const QList<KDUpdater::Update*> &updates, + const GetRepositoriesMetaInfoJob &metaInfoJob) +{ + verbose() << "entered create components in installer" << std::endl; + + emit componentsAboutToBeCleared(); + + qDeleteAll(d->m_components); + qDeleteAll(d->m_updaterComponents); + d->m_components.clear(); + d->m_updaterComponents.clear(); + d->m_packageManagerComponents.clear(); + d->m_componentHash.clear(); + + KDUpdater::Application &updaterApp = *d->m_app; + KDUpdater::PackagesInfo &packagesInfo = *updaterApp.packagesInfo(); + + if (isUninstaller() || isPackageManager()) { + if (!setAndParseLocalComponentsFile(packagesInfo)) + return; + packagesInfo.setApplicationName(d->m_settings.applicationName()); + packagesInfo.setApplicationVersion(d->m_settings.applicationVersion()); + } + + bool containsImportantUpdates = false; + QMap<QInstaller::Component*, QString> scripts; + QMap<QString, QInstaller::Component*> components; + QList<Component*> componentsToSelectUpdater, componentsToSelectInstaller; + + if (metaInfoJob.error() == KDJob::UserDefinedError) { + foreach (const KDUpdater::PackageInfo &info, packagesInfo.packageInfos()) { + QScopedPointer<QInstaller::Component> component(new QInstaller::Component(this)); + component->setValue(QLatin1String("Name"), info.name); + component->setValue(QLatin1String("DisplayName"), info.title); + component->setValue(QLatin1String("Description"), info.description); + component->setValue(QLatin1String("UncompressedSize"), + QString::number(info.uncompressedSize)); + component->setValue(QLatin1String("Version"), info.version); + component->setValue(QLatin1String("Virtual"), + info.virtualComp ? QLatin1String ("true") : QLatin1String ("false")); + QString dependstr = QLatin1String(""); + foreach (const QString& val, info.dependencies) + dependstr += val + QLatin1String(","); + if (info.dependencies.count() > 0) + dependstr.chop(1); + component->setValue(QLatin1String("Dependencies"), dependstr); + if (info.forcedInstallation) + component->setValue(QLatin1String("ForcedInstallation"), + info.forcedInstallation ? QLatin1String ("true") : QLatin1String ("false")); + + if (components.contains(info.name)) { + qCritical("Could not register component! Component with identifier %s already " + "registered", qPrintable(info.name)); + } else { + components.insert(info.name, component.take()); + } + } + } else { + foreach (KDUpdater::Update * const update, updates) { + const QString newComponentName = update->data(QLatin1String("Name")).toString(); + const int indexOfPackage = packagesInfo.findPackageInfo(newComponentName); + + const QString localPath = QInstaller::pathFromUrl(update->sourceInfo().url); + static QString lastLocalPath; + if (lastLocalPath != localPath) + verbose() << "Url is : " << localPath << std::endl; + + lastLocalPath = localPath; + QScopedPointer<QInstaller::Component> component(new QInstaller::Component(update, this)); + if (indexOfPackage > -1) { + component->setValue(QLatin1String("InstalledVersion"), + packagesInfo.packageInfo(indexOfPackage).version); + } + + const Repository repo = metaInfoJob.repositoryForTemporaryDirectory(localPath); + component->setRepositoryUrl(repo.url()); + + component->setValue(QLatin1String("Important"), + update->data(QLatin1String("Important")).toString()); + + component->setValue(QLatin1String("ForcedInstallation"), + update->data(QLatin1String("ForcedInstallation")).toString()); + + component->setValue(QLatin1String("UpdateText"), + update->data(QLatin1String("UpdateText")).toString()); + + component->setValue(QLatin1String("RequiresAdminRights"), + update->data(QLatin1String("RequiresAdminRights")).toString()); + + component->setValue(QLatin1String("NewComponent"), + update->data(QLatin1String("NewComponent")).toString()); + + const QStringList uis = update->data(QLatin1String("UserInterfaces")).toString() + .split(QString::fromLatin1(","), QString::SkipEmptyParts); + if (!uis.isEmpty()) { + verbose() << "Loading User Interface definitions for component " << newComponentName + << std::endl; + component->loadUserInterfaces(QDir(QString::fromLatin1("%1/%2").arg(localPath, + newComponentName)), uis); + } + + const QStringList qms = update->data(QLatin1String("Translations")).toString() + .split(QString::fromLatin1(","), QString::SkipEmptyParts); + if (!qms.isEmpty()) { + verbose() << "Loading translations for component " << newComponentName << std::endl; + component->loadTranslations(QDir(QString::fromLatin1("%1/%2").arg(localPath, + newComponentName)), qms); + } + + QHash<QString, QVariant> licenseHash = update->data(QLatin1String("Licenses")).toHash(); + if (!licenseHash.isEmpty()) { + verbose() << "Loading licenses for component " << newComponentName << std::endl; + component->loadLicenses(QString::fromLatin1("%1/%2/").arg(localPath, + newComponentName), licenseHash); + } + + bool isUpdate = true; + // the package manager should preselect the currently installed packages + if (isPackageManager()) { + const bool selected = indexOfPackage > -1; + + component->setValue(QLatin1String("PreviousState"), + selected ? QLatin1String("Installed") : QLatin1String("Uninstalled")); + component->setValue(QLatin1String("CurrentState"), + component->value(QLatin1String("PreviousState"))); + + if (selected) + componentsToSelectInstaller.append(component.data()); + + const QString pkgVersion = packagesInfo.packageInfo(indexOfPackage).version; + const QString updateVersion = update->data(QLatin1String("Version")).toString(); + if (KDUpdater::compareVersion(updateVersion, pkgVersion) <= 0) + isUpdate = false; + + // It is quite possible that we may have already installed the update. + // Lets check the last update date of the package and the release date + // of the update. This way we can compare and figure out if the update + // has been installed or not. + const QDate updateDate = update->data(QLatin1String("ReleaseDate")).toDate(); + const QDate pkgDate = packagesInfo.packageInfo(indexOfPackage).lastUpdateDate; + if (pkgDate > updateDate) + isUpdate = false; + } + + if (!components.contains(newComponentName)) { + const QString script = update->data(QLatin1String("Script")).toString(); + if (!script.isEmpty()) { + scripts.insert(component.data(), QString::fromLatin1("%1/%2/%3").arg(localPath, + newComponentName, script) ); + } + + Component *tmpComponent = component.data(); + const bool isInstalled = tmpComponent->value(QLatin1String("PreviousState")) + == QLatin1String("Installed"); + const bool isNewComponent = tmpComponent->value(QLatin1String("NewComponent")) + == QLatin1String("true") ? true : false; + const bool newPackageForUpdater = !isInstalled && isNewComponent; + isUpdate = isUpdate && isInstalled; + + if (newPackageForUpdater) { + d->m_updaterComponents.push_back(component.take()); + d->m_componentHash[newComponentName] = tmpComponent; + } else { + components.insert(newComponentName, component.take()); + } + + if (isPackageManager() && (isUpdate || newPackageForUpdater)) { + if (update->data(QLatin1String("Important")).toBool()) + containsImportantUpdates = true; + componentsToSelectUpdater.append(tmpComponent); + d->m_packageManagerComponents.push_back(tmpComponent); + } + } else { + qCritical("Could not register component! Component with identifier %s already " + "registered", qPrintable(newComponentName)); + } + } + } + + if (containsImportantUpdates) { + foreach (QInstaller::Component *c, d->m_packageManagerComponents) { + if (c->value(QLatin1String("Important")).toLower() == QLatin1String ("false") + || c->value(QLatin1String("Important")).isEmpty()) { + if (isPackageManager()) + d->m_packageManagerComponents.removeAll(c); + componentsToSelectUpdater.removeAll(c); + } + } + } + + // now append all components to their respective parents + QMap<QString, QInstaller::Component*>::const_iterator it; + for (it = components.begin(); !d->linearComponentList && it != components.end(); ++it) { + QInstaller::Component* const comp = *it; + QString id = it.key(); + while (!id.isEmpty() && comp->parentComponent() == 0) { + id = id.section(QChar::fromLatin1('.'), 0, -2); + if (components.contains(id)) + components[id]->appendComponent(comp); + } + } + + // append all components w/o parent to the direct list + for (it = components.begin(); it != components.end(); ++it) { + if (d->linearComponentList || (*it)->parentComponent() == 0) + appendComponent(*it); + } + + // after everything is set up, load the scripts + QMapIterator< QInstaller::Component*, QString > scriptIt(scripts); + while (scriptIt.hasNext()) { + scriptIt.next(); + QInstaller::Component* const component = scriptIt.key(); + const QString script = scriptIt.value(); + verbose() << "Loading script for component " << component->name() << " (" << script << ")" + << std::endl; + component->loadComponentScript(script); + } + + // select all components in the updater model + foreach (QInstaller::Component* const i, componentsToSelectUpdater) + i->setSelected(true, UpdaterMode, Component::InitializeComponentTreeSelectMode); + + // select all components in the package manager model + foreach (QInstaller::Component* const i, componentsToSelectInstaller) + i->setSelected(true, InstallerMode, Component::InitializeComponentTreeSelectMode); + + emit updaterComponentsAdded(d->m_packageManagerComponents); + emit componentsAdded(d->m_components); +} + +void Installer::appendComponent(Component *component) +{ + d->m_components.append(component); + d->m_componentHash[component->name()] = component; + emit componentAdded(component); +} + +int Installer::componentCount(RunModes runMode) const +{ + if (runMode == UpdaterMode) + return d->m_packageManagerComponents.size(); + return d->m_components.size(); +} + +Component *Installer::component(int i, RunModes runMode) const +{ + if (runMode == UpdaterMode) + return d->m_packageManagerComponents.at(i); + return d->m_components.at(i); +} + +Component *Installer::component(const QString &name) const +{ + return d->m_componentHash.contains(name) ? d->m_componentHash[name] : 0; +} + +QList<Component*> Installer::components(bool recursive, RunModes runMode) const +{ + if (runMode == UpdaterMode) + return d->m_packageManagerComponents; + + if (!recursive) + return d->m_components; + + QList<Component*> result; + QList<Component*>::const_iterator it; + for (it = d->m_components.begin(); it != d->m_components.end(); ++it) { + result.push_back(*it); + result += (*it)->components(true); + } + + if (runMode == AllMode) { + for (it = d->m_updaterComponents.begin(); it != d->m_updaterComponents.end(); ++it) { + result.push_back(*it); + result += (*it)->components(false); + } + } + + return result; +} + +QList<Component*> Installer::componentsToInstall(bool recursive, bool sort, RunModes runMode) const +{ + QList<Component*> availableComponents = components(recursive, runMode); + if (sort) { + std::sort(availableComponents.begin(), availableComponents.end(), + Component::PriorityLessThan()); + } + + QList<Component*>::const_iterator it; + QList<Component*> componentsToInstall; + for (it = availableComponents.begin(); it != availableComponents.end(); ++it) { + Component* const comp = *it; + if (!comp->isSelected(runMode)) + continue; + + // it was already installed before, so don't add it + if (comp->value(QLatin1String("PreviousState")) == QLatin1String("Installed") + && runMode == InstallerMode) // TODO: is the last condition right ???? + continue; + + appendComponentAndMissingDependencies(componentsToInstall, comp); + } + + return componentsToInstall; +} + +static bool componentMatches(const Component *component, const QString &name, + const QString& version = QString()) +{ + if (!name.isEmpty() && component->name() != name) + return false; + + if (version.isEmpty()) + return true; + + return Installer::versionMatches(component->value(QLatin1String("Version")), version); +} + +static Component* subComponentByName(const Installer *installer, const QString &name, + const QString &version = QString(), Component *check = 0) +{ + if (check != 0 && componentMatches(check, name, version)) + return check; + + const QList<Component*> comps = check == 0 ? installer->components() : check->components(); + for (QList<Component*>::const_iterator it = comps.begin(); it != comps.end(); ++it) { + Component* const result = subComponentByName(installer, name, version, *it); + if (result != 0) + return result; + } + + const QList<Component*> uocomps = + check == 0 ? installer->components(false, UpdaterMode) : check->components(false, UpdaterMode); + for (QList<Component*>::const_iterator it = uocomps.begin(); it != uocomps.end(); ++it) { + Component* const result = subComponentByName(installer, name, version, *it); + if (result != 0) + return result; + } + + return 0; +} + +void Installer::setLinearComponentList(bool showlinear) +{ + d->linearComponentList = showlinear; +} + +bool Installer::hasLinearComponentList() const +{ + return d->linearComponentList; +} + +/*! + Returns a component matching \a name. \a name can also contains a version requirement. + E.g. "com.nokia.sdk.qt" returns any component with that name, "com.nokia.sdk.qt->=4.5" requires + the returned component to have at least version 4.5. + If no component matches the requirement, 0 is returned. +*/ +Component* Installer::componentByName(const QString &name) const +{ + if (name.contains(QChar::fromLatin1('-'))) { + // the last part is considered to be the version, then + const QString version = name.section(QLatin1Char('-'), 1); + return subComponentByName(this, name.section(QLatin1Char('-'), 0, 0), version); + } + + QHash< QString, QInstaller::Component* >::ConstIterator it = d->m_componentHash.constFind(name); + Component * comp = 0; + if (it != d->m_componentHash.constEnd()) + comp = *it; + if (d->m_updaterComponents.contains(comp)) + return comp; + + return subComponentByName(this, name); +} + +/*! + Returns a list of packages depending on \a component. +*/ +QList<Component*> Installer::dependees(const Component *component) const +{ + QList<Component*> result; + + const QList<Component*> allComponents = components(true, AllMode); + for (QList<Component*>::const_iterator it = allComponents.begin(); it != allComponents.end(); ++it) { + Component* const c = *it; + + const QStringList deps = c->value(QString::fromLatin1("Dependencies")) + .split(QChar::fromLatin1(','), QString::SkipEmptyParts); + + const QLatin1Char dash('-'); + for (QStringList::const_iterator it2 = deps.begin(); it2 != deps.end(); ++it2) { + // the last part is considered to be the version, then + const QString id = it2->contains(dash) ? it2->section(dash, 0, 0) : *it2; + const QString version = it2->contains(dash) ? it2->section(dash, 1) : QString(); + if (componentMatches(component, id, version)) + result.push_back(c); + } + } + + return result; +} + +InstallerSettings Installer::settings() const +{ + return d->m_settings; +} + +/*! + Returns a list of dependencies for \a component. + If there's a dependency which cannot be fullfilled, the list contains 0 values. +*/ +QList<Component*> Installer::dependencies(const Component *component, + QStringList *missingPackageNames) const +{ + QList<Component*> result; + const QStringList deps = component->value(QString::fromLatin1("Dependencies")) + .split(QChar::fromLatin1(','), QString::SkipEmptyParts); + + for (QStringList::const_iterator it = deps.begin(); it != deps.end(); ++it) { + const QString name = *it; + Component* comp = componentByName(*it); + if (!comp && missingPackageNames) + missingPackageNames->append(name); + else + result.push_back(comp); + } + return result; +} + +/*! + Returns the list of all missing (not installed) dependencies for \a component. +*/ +QList<Component*> Installer::missingDependencies(const Component *component) const +{ + QList<Component*> result; + const QStringList deps = component->value(QString::fromLatin1("Dependencies")) + .split(QChar::fromLatin1(','), QString::SkipEmptyParts); + + const QLatin1Char dash('-'); + for (QStringList::const_iterator it = deps.begin(); it != deps.end(); ++it) { + const bool containsVersionString = it->contains(dash); + const QString version = containsVersionString ? it->section(dash, 1) : QString(); + const QString name = containsVersionString ? it->section(dash, 0, 0) : *it; + + bool installed = false; + const QList<Component*> compList = components(true); + foreach (const Component* comp, compList) { + if (!name.isEmpty() && comp->name() == name && !version.isEmpty()) { + if (Installer::versionMatches(comp->value(QLatin1String("InstalledVersion")), version)) + installed = true; + } else if (comp->name() == name) { + installed = true; + } + } + + foreach (const Component *comp, d->m_updaterComponents) { + if (!name.isEmpty() && comp->name() == name && !version.isEmpty()) { + if (Installer::versionMatches(comp->value(QLatin1String("InstalledVersion")), version)) + installed = true; + } else if (comp->name() == name) { + installed = true; + } + } + + if (!installed) { + if (Component *comp = componentByName(name)) + result.push_back(comp); + } + } + return result; +} + +/*! + This method tries to gain admin rights. On success, it returns true. +*/ +bool Installer::gainAdminRights() +{ + if (AdminAuthorization::hasAdminRights()) + return true; + + d->engineClientHandler->setActive(true); + if (!d->engineClientHandler->isActive()) + throw Error(QObject::tr("Error while elevating access rights.")); + return true; +} + +/*! + This method drops gained admin rights. +*/ +void Installer::dropAdminRights() +{ + d->engineClientHandler->setActive(false); +} + +/*! + Return true, if a process with \a name is running. On Windows, the comparision is case-insensitive. +*/ +bool Installer::isProcessRunning(const QString &name) const +{ + QList<KDSysInfo::ProcessInfo>::const_iterator it; + const QList<KDSysInfo::ProcessInfo> processes = KDSysInfo::runningProcesses(); + for (it = processes.begin(); it != processes.end(); ++it) { +#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; + const QFileInfo fi(it->name); + if (fi.fileName().toLower() == name.toLower() || fi.baseName().toLower() == name.toLower()) + return true; +#endif + } + return false; +} + +/*! + Executes a program. + + \param program The program that should be executed. + \param arguments Optional list of arguments. + \param stdIn Optional stdin the program reads. + \return If the command could not be executed, an empty QList, otherwise the output of the + command as first item, the return code as second item. + \note On Unix, the output is just the output to stdout, not to stderr. +*/ +QList<QVariant> Installer::execute(const QString &program, const QStringList &arguments, + const QString &stdIn) const +{ + QProcess p; + p.start(program, arguments, stdIn.isNull() ? QIODevice::ReadOnly : QIODevice::ReadWrite); + if (!p.waitForStarted()) + return QList< QVariant >(); + + if (!stdIn.isNull()) { + p.write(stdIn.toLatin1()); + p.closeWriteChannel(); + } + + QEventLoop loop; + connect(&p, SIGNAL(finished(int, QProcess::ExitStatus)), &loop, SLOT(quit())); + loop.exec(); + + return QList< QVariant >() << QString::fromLatin1(p.readAllStandardOutput()) << p.exitCode(); +} + +/*! + Returns an environment variable. +*/ +QString Installer::environmentVariable(const QString &name) const +{ +#ifdef Q_WS_WIN + const LPCWSTR n = (LPCWSTR) name.utf16(); + LPTSTR buff = (LPTSTR) malloc(4096 * sizeof(TCHAR)); + DWORD getenvret = GetEnvironmentVariable(n, buff, 4096); + const QString actualValue = getenvret != 0 + ? QString::fromUtf16((const unsigned short *) buff) : QString(); + free(buff); + return actualValue; +#else + const char *pPath = name.isEmpty() ? 0 : getenv(name.toLatin1()); + return pPath ? QLatin1String(pPath) : QString(); +#endif +} + +/*! + Instantly performns an operation \a name with \a arguments. + \sa Component::addOperation +*/ +bool Installer::performOperation(const QString &name, const QStringList &arguments) +{ + QScopedPointer<KDUpdater::UpdateOperation> op(KDUpdater::UpdateOperationFactory::instance() + .create(name)); + if (!op.data()) + return false; + + op->setArguments(arguments); + op->backup(); + if (!performOperationThreaded(op.data())) { + performOperationThreaded(op.data(), Undo); + return false; + } + return true; +} + +/*! + Returns true when \a version matches the \a requirement. + \a requirement can be a fixed version number or it can be prefix by the comparaters '>', '>=', + '<', '<=' and '='. +*/ +bool Installer::versionMatches(const QString &version, const QString &requirement) +{ + QRegExp compEx(QLatin1String("([<=>]+)(.*)")); + const QString comparator = compEx.exactMatch(requirement) ? compEx.cap(1) : QString::fromLatin1("="); + const QString ver = compEx.exactMatch(requirement) ? compEx.cap(2) : requirement; + + const bool allowEqual = comparator.contains(QLatin1Char('=')); + const bool allowLess = comparator.contains(QLatin1Char('<')); + const bool allowMore = comparator.contains(QLatin1Char('>')); + + if (allowEqual && version == ver) + return true; + + if (allowLess && KDUpdater::compareVersion(ver, version) > 0) + return true; + + if (allowMore && KDUpdater::compareVersion(ver, version) < 0) + return true; + + return false; +} + +/*! + Finds a library named \a name in \a pathes. + If \a pathes is empty, it gets filled with platform dependent default pathes. + The resulting path is stored in \a library. + This method can be used by scripts to check external dependencies. +*/ +QString Installer::findLibrary(const QString &name, const QStringList &pathes) +{ + QStringList findPathes = pathes; +#if defined(Q_WS_WIN) + return findPath(QString::fromLatin1("%1.lib").arg(name), findPathes); +#elif defined(Q_WS_MAC) + if (findPathes.isEmpty()) { + findPathes.push_back(QLatin1String("/lib")); + findPathes.push_back(QLatin1String("/usr/lib")); + findPathes.push_back(QLatin1String("/usr/local/lib")); + findPathes.push_back(QLatin1String("/opt/local/lib")); + } + + const QString dynamic = findPath(QString::fromLatin1("lib%1.dylib").arg(name), findPathes); + if (!dynamic.isEmpty()) + return dynamic; + return findPath(QString::fromLatin1("lib%1.a").arg(name), findPathes); +#else + if (findPathes.isEmpty()) { + findPathes.push_back(QLatin1String("/lib")); + findPathes.push_back(QLatin1String("/usr/lib")); + findPathes.push_back(QLatin1String("/usr/local/lib")); + findPathes.push_back(QLatin1String("/opt/local/lib")); + } + const QString dynamic = findPath(QString::fromLatin1("lib%1.so*").arg(name), findPathes); + if (!dynamic.isEmpty()) + return dynamic; + return findPath(QString::fromLatin1("lib%1.a").arg(name), findPathes); +#endif +} + +/*! + Tries to find a file name \a name in one of \a pathes. + The resulting path is stored in \a path. + This method can be used by scripts to check external dependencies. +*/ +QString Installer::findPath(const QString &name, const QStringList &pathes) +{ + for (QStringList::const_iterator it = pathes.begin(); it != pathes.end(); ++it) { + const QDir dir(*it); + const QStringList entries = dir.entryList(QStringList() << name, QDir::Files | QDir::Hidden); + if (entries.isEmpty()) + continue; + + return dir.absoluteFilePath(entries.first()); + } + return QString(); +} + +/*! + sets the "installerbase" binary to use when writing the package manager/uninstaller. + Set this if an update to installerbase is available. + If not set, the executable segment of the running un/installer will be used. +*/ +void Installer::setInstallerBaseBinary(const QString &path) +{ + d->m_forceRestart = true; + d->installerBaseBinaryUnreplaced = path; +} + +/*! + Returns the installer value for \a key. If \a key is not known to the system, \a defaultValue is + returned. Additionally, on Windows, \a key can be a registry key. +*/ +QString Installer::value(const QString &key, const QString &defaultValue) const +{ +#ifdef Q_WS_WIN + if (!d->m_vars.contains(key)) { + static const QRegExp regex(QLatin1String("\\\\|/")); + const QString filename = key.section(regex, 0, -2); + const QString regKey = key.section(regex, -1); + const QSettings registry(filename, QSettings::NativeFormat); + if (!filename.isEmpty() && !regKey.isEmpty() && registry.contains(regKey)) + return registry.value(regKey).toString(); + } +#else + if (key == QLatin1String("TargetDir")) { + const QString dir = d->m_vars.value(key, defaultValue); + if (dir.startsWith(QLatin1String("~/"))) + return QDir::home().absoluteFilePath(dir.mid(2)); + else + return dir; + } +#endif + return d->m_vars.value(key, defaultValue); +} + +/*! + Sets the installer value for \a key to \a value. +*/ +void Installer::setValue(const QString &key, const QString &value) +{ + if (d->m_vars.value(key) == value) + return; + + d->m_vars.insert(key, value); + emit valueChanged(key, value); +} + +/*! + Returns true, when the installer contains a value for \a key. +*/ +bool Installer::containsValue(const QString &key) const +{ + return d->m_vars.contains(key); +} + +void Installer::setSharedFlag(const QString &key, bool value) +{ + d->m_sharedFlags.insert(key, value); +} + +bool Installer::sharedFlag(const QString &key) const +{ + return d->m_sharedFlags.value(key, false); +} + +bool Installer::isVerbose() const +{ + return QInstaller::isVerbose(); +} + +void Installer::setVerbose(bool on) +{ + QInstaller::setVerbose(on); +} + +int Installer::status() const +{ + return d->m_status; +} +/*! + returns true if at least one complete installation/update + was successfull, even if the user cancelled the newest + installation process. +*/ +bool Installer::finishedWithSuccess() const +{ + return (d->m_status == InstallerSucceeded) || d->m_needToWriteUninstaller; +} + +void Installer::interrupt() +{ + verbose() << "INTERRUPT INSTALLER" << std::endl; + d->setStatus(InstallerCanceledByUser); + emit installationInterrupted(); +} + +void Installer::setCanceled() +{ + d->setStatus(InstallerCanceledByUser); +} + +/*! + Replaces all variables within \a str by their respective values and + returns the result. +*/ +QString Installer::replaceVariables(const QString &str) const +{ + return d->replaceVariables(str); +} + +/*! + Replaces all variables in any of \a str by their respective values and + returns the results. + \overload +*/ +QStringList Installer::replaceVariables(const QStringList &str) const +{ + QStringList result; + for (QStringList::const_iterator it = str.begin(); it != str.end(); ++it) + result.push_back(d->replaceVariables(*it)); + + return result; +} + +/*! + Replaces all variables within \a ba by their respective values and + returns the result. + \overload +*/ +QByteArray Installer::replaceVariables(const QByteArray &ba) const +{ + return d->replaceVariables(ba); +} + +/*! + Returns the path to the installer binary. +*/ +QString Installer::installerBinaryPath() const +{ + return d->installerBinaryPath(); +} + +/*! + Returns true when this is the installer running. +*/ +bool Installer::isInstaller() const +{ + return d->isInstaller(); +} + +/*! + Returns true when this is the uninstaller running. +*/ +bool Installer::isUninstaller() const +{ + return d->isUninstaller(); +} + +/*! + Returns true when this is the package manager running. +*/ +bool Installer::isPackageManager() const +{ + return d->isPackageManager(); +} + + +/*! + Returns true if this is an offline-only installer. +*/ +bool Installer::isOfflineOnly() const +{ + QSettings confInternal(QLatin1String(":/config/config-internal.ini"), QSettings::IniFormat); + return confInternal.value(QLatin1String("offlineOnly")).toBool(); +} + +void Installer::setPackageManager() +{ + d->packageManagingMode = true; +} + +/*! + Returns thrue when this is neither an installer nor an uninstaller running. + Must be an updater, then. +*/ +bool Installer::isUpdater() const +{ + return !d->isInstaller() && !d->isUninstaller(); +} + +/*! + Runs the installer. Returns true on success, false otherwise. +*/ +bool Installer::runInstaller() +{ + try { + d->runInstaller(); + return true; + } catch (...) { + return false; + } +} + +/*! + Runs the uninstaller. Returns true on success, false otherwise. +*/ +bool Installer::runUninstaller() +{ + try { + d->runUninstaller(); + return true; + } catch (...) { + return false; + } +} + +/*! + Runs the package updater. Returns true on success, false otherwise. +*/ +bool Installer::runPackageUpdater() +{ + try { + d->runPackageUpdater(); + return true; + } catch (...) { + return false; + } +} + +/*! + \internal + Calls languangeChanged on all components. +*/ +void Installer::languageChanged() +{ + const QList<Component*> comps = components(true); + foreach (Component* component, comps) + component->languageChanged(); +} + +/*! + Runs the installer or uninstaller, depending on the type of this binary. +*/ +bool Installer::run() +{ + try { + if (isInstaller()) + d->runInstaller(); + else if (isUninstaller()) + d->runUninstaller(); + else if (isPackageManager()) + d->runPackageUpdater(); + return true; + } catch (const Error &err) { + verbose() << "Caught Installer Error: " << err.message() << std::endl; + return false; + } +} + +/*! + Returns the path name of the ininstaller binary. +*/ +QString Installer::uninstallerName() const +{ + return d->uninstallerName(); +} + +bool Installer::setAndParseLocalComponentsFile(KDUpdater::PackagesInfo &packagesInfo) +{ + packagesInfo.setFileName(d->localComponentsXmlPath()); + const QString localComponentsXml = d->localComponentsXmlPath(); + + // handle errors occured by loading components.xml + QFileInfo componentFileInfo(localComponentsXml); + int silentRetries = d->m_silentRetries; + while (!componentFileInfo.exists()) { + if (silentRetries > 0) { + --silentRetries; + } else { + Status status = handleComponentsFileSetOrParseError(localComponentsXml); + if (status == InstallerCanceledByUser) + return false; + } + packagesInfo.setFileName(localComponentsXml); + } + + silentRetries = d->m_silentRetries; + while (packagesInfo.error() != KDUpdater::PackagesInfo::NoError) { + if (silentRetries > 0) { + --silentRetries; + } else { + Status status = handleComponentsFileSetOrParseError(localComponentsXml); + if (status == InstallerCanceledByUser) + return false; + } + packagesInfo.setFileName(localComponentsXml); + } + + silentRetries = d->m_silentRetries; + while (packagesInfo.error() != KDUpdater::PackagesInfo::NoError) { + if (silentRetries > 0) { + --silentRetries; + } else { + bool retry = false; + if (packagesInfo.error() != KDUpdater::PackagesInfo::InvalidContentError + && packagesInfo.error() != KDUpdater::PackagesInfo::InvalidXmlError) { + retry = true; + } + Status status = handleComponentsFileSetOrParseError(componentFileInfo.fileName(), + packagesInfo.errorString(), retry); + if (status == InstallerCanceledByUser) + return false; + } + packagesInfo.setFileName(localComponentsXml); + } + + return true; +} + +Installer::Status Installer::handleComponentsFileSetOrParseError(const QString &arg1, + const QString &arg2, bool withRetry) +{ + QMessageBox::StandardButtons buttons = QMessageBox::Cancel; + if (withRetry) + buttons |= QMessageBox::Retry; + + const QMessageBox::StandardButton button = + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("Error loading component.xml"), tr("Loading error"), + tr(arg2.isEmpty() ? "Could not load %1" : "Could not load %1 : %2").arg(arg1, arg2), + buttons); + + if (button == QMessageBox::Cancel) { + d->m_status = InstallerFailed; + return InstallerCanceledByUser; + } + return InstallerUnfinished; +} + +#include "qinstaller.moc" |