// // AppDelegate.cpp // StackManagerQt/src // // Created by Mohammed Nafees on 06/27/14. // Copyright (c) 2014 High Fidelity. All rights reserved. // #include #include "AppDelegate.h" #include "BackgroundProcess.h" #include "GlobalData.h" #include "DownloadManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include const QString HIGH_FIDELITY_API_URL = "https://metaverse.highfidelity.com/api/v1"; const QString CHECK_BUILDS_URL = "https://highfidelity.com/builds.xml"; // Use a custom User-Agent to avoid ModSecurity filtering, e.g. by hosting providers. const QByteArray HIGH_FIDELITY_USER_AGENT = "Mozilla/5.0 (HighFidelity)"; const int VERSION_CHECK_INTERVAL_MS = 86400000; // a day const int WAIT_FOR_CHILD_MSECS = 5000; void signalHandler(int param) { AppDelegate* app = AppDelegate::getInstance(); app->quit(); } static QTextStream* outStream = NULL; void myMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { Q_UNUSED(context); QString dateTime = QDateTime::currentDateTime().toString("dd/MM/yyyy hh:mm:ss"); QString txt = QString("[%1] ").arg(dateTime); //in this function, you can write the message to any stream! switch (type) { case QtDebugMsg: fprintf(stdout, "Debug: %s\n", qPrintable(msg)); txt += msg; break; case QtWarningMsg: fprintf(stdout, "Warning: %s\n", qPrintable(msg)); txt += msg; break; case QtCriticalMsg: fprintf(stdout, "Critical: %s\n", qPrintable(msg)); txt += msg; break; case QtFatalMsg: fprintf(stdout, "Fatal: %s\n", qPrintable(msg)); txt += msg; } if (outStream) { *outStream << txt << endl; } } AppDelegate::AppDelegate(int argc, char* argv[]) : QApplication(argc, argv), _qtReady(false), _dsReady(false), _dsResourcesReady(false), _acReady(false), _domainServerProcess(NULL), _acMonitorProcess(NULL), _domainServerName("localhost") { // be a signal handler for SIGTERM so we can stop child processes if we get it signal(SIGTERM, signalHandler); // look for command-line options parseCommandLine(); setApplicationName("Stack Manager"); setOrganizationName("High Fidelity"); setOrganizationDomain("io.highfidelity.StackManager"); QFile* logFile = new QFile("last_run_log", this); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { qDebug() << "Failed to open log file. Will not be able to write STDOUT/STDERR to file."; } else { outStream = new QTextStream(logFile); } qInstallMessageHandler(myMessageHandler); _domainServerProcess = new BackgroundProcess(GlobalData::getInstance().getDomainServerExecutablePath(), this); _acMonitorProcess = new BackgroundProcess(GlobalData::getInstance().getAssignmentClientExecutablePath(), this); _manager = new QNetworkAccessManager(this); _window = new MainWindow(); createExecutablePath(); downloadLatestExecutablesAndRequirements(); _checkVersionTimer.setInterval(0); connect(&_checkVersionTimer, SIGNAL(timeout()), this, SLOT(checkVersion())); _checkVersionTimer.start(); connect(this, &QApplication::aboutToQuit, this, &AppDelegate::stopStack); } AppDelegate::~AppDelegate() { QHash::iterator it = _scriptProcesses.begin(); qDebug() << "Stopping scripted assignment-client processes prior to quit."; while (it != _scriptProcesses.end()) { BackgroundProcess* backgroundProcess = it.value(); // remove from the script processes hash it = _scriptProcesses.erase(it); // make sure the process is dead backgroundProcess->terminate(); backgroundProcess->waitForFinished(); backgroundProcess->deleteLater(); } qDebug() << "Stopping domain-server process prior to quit."; _domainServerProcess->terminate(); _domainServerProcess->waitForFinished(); qDebug() << "Stopping assignment-client process prior to quit."; _acMonitorProcess->terminate(); _acMonitorProcess->waitForFinished(); _domainServerProcess->deleteLater(); _acMonitorProcess->deleteLater(); _window->deleteLater(); delete outStream; outStream = NULL; } void AppDelegate::parseCommandLine() { QCommandLineParser parser; parser.setApplicationDescription("High Fidelity Stack Manager"); parser.addHelpOption(); const QCommandLineOption helpOption = parser.addHelpOption(); const QCommandLineOption hifiBuildDirectoryOption("b", "Path to build of hifi", "build-directory"); parser.addOption(hifiBuildDirectoryOption); if (!parser.parse(QCoreApplication::arguments())) { qCritical() << parser.errorText() << endl; parser.showHelp(); Q_UNREACHABLE(); } if (parser.isSet(helpOption)) { parser.showHelp(); Q_UNREACHABLE(); } if (parser.isSet(hifiBuildDirectoryOption)) { const QString hifiBuildDirectory = parser.value(hifiBuildDirectoryOption); qDebug() << "hifiBuildDirectory=" << hifiBuildDirectory << "\n"; GlobalData::getInstance().setHifiBuildDirectory(hifiBuildDirectory); } } void AppDelegate::toggleStack(bool start) { toggleDomainServer(start); toggleAssignmentClientMonitor(start); toggleScriptedAssignmentClients(start); emit stackStateChanged(start); } void AppDelegate::toggleDomainServer(bool start) { if (start) { _domainServerProcess->start(QStringList()); _window->getLogsWidget()->addTab(_domainServerProcess->getLogViewer(), "Domain Server"); if (_domainServerID.isEmpty()) { // after giving the domain server some time to set up, ask for its ID QTimer::singleShot(1000, this, SLOT(requestDomainServerID())); } } else { _domainServerProcess->terminate(); _domainServerProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); _domainServerProcess->kill(); } } void AppDelegate::toggleAssignmentClientMonitor(bool start) { if (start) { _acMonitorProcess->start(QStringList() << "--min" << "5"); _window->getLogsWidget()->addTab(_acMonitorProcess->getLogViewer(), "Assignment Clients"); } else { _acMonitorProcess->terminate(); _acMonitorProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); _acMonitorProcess->kill(); } } void AppDelegate::toggleScriptedAssignmentClients(bool start) { foreach(BackgroundProcess* scriptProcess, _scriptProcesses) { if (start) { scriptProcess->start(scriptProcess->getLastArgList()); } else { scriptProcess->terminate(); scriptProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); scriptProcess->kill(); } } } int AppDelegate::startScriptedAssignment(const QUuid& scriptID, const QString& pool) { BackgroundProcess* scriptProcess = _scriptProcesses.value(scriptID); if (!scriptProcess) { QStringList argList = QStringList() << "-t" << "2"; if (!pool.isEmpty()) { argList << "--pool" << pool; } scriptProcess = new BackgroundProcess(GlobalData::getInstance().getAssignmentClientExecutablePath(), this); scriptProcess->start(argList); qint64 processID = scriptProcess->processId(); _scriptProcesses.insert(scriptID, scriptProcess); _window->getLogsWidget()->addTab(scriptProcess->getLogViewer(), "Scripted Assignment " + QString::number(processID)); } else { scriptProcess->QProcess::start(); } return scriptProcess->processId(); } void AppDelegate::stopScriptedAssignment(BackgroundProcess* backgroundProcess) { _window->getLogsWidget()->removeTab(_window->getLogsWidget()->indexOf(backgroundProcess->getLogViewer())); backgroundProcess->terminate(); backgroundProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); backgroundProcess->kill(); } void AppDelegate::stopScriptedAssignment(const QUuid& scriptID) { BackgroundProcess* processValue = _scriptProcesses.take(scriptID); if (processValue) { stopScriptedAssignment(processValue); } } void AppDelegate::requestDomainServerID() { // ask the domain-server for its ID so we can update the accessible name emit domainAddressChanged(); QUrl domainIDURL = GlobalData::getInstance().getDomainServerBaseUrl() + "/id"; qDebug() << "Requesting domain server ID from" << domainIDURL.toString(); QNetworkReply* idReply = _manager->get(QNetworkRequest(domainIDURL)); connect(idReply, &QNetworkReply::finished, this, &AppDelegate::handleDomainIDReply); } const QString AppDelegate::getServerAddress() const { return "hifi://" + _domainServerName; } void AppDelegate::handleDomainIDReply() { QNetworkReply* reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { _domainServerID = QString(reply->readAll()); if (!_domainServerID.isEmpty()) { if (!QUuid(_domainServerID).isNull()) { qDebug() << "The domain server ID is" << _domainServerID; qDebug() << "Asking High Fidelity API for associated domain name."; // fire off a request to high fidelity API to see if this domain exists with them QUrl domainGetURL = HIGH_FIDELITY_API_URL + "/domains/" + _domainServerID; QNetworkReply* domainGetReply = _manager->get(QNetworkRequest(domainGetURL)); connect(domainGetReply, &QNetworkReply::finished, this, &AppDelegate::handleDomainGetReply); } else { emit domainServerIDMissing(); } } } else { qDebug() << "Error getting domain ID from domain-server - " << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << reply->errorString(); } } void AppDelegate::handleDomainGetReply() { QNetworkReply* reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll()); QJsonObject domainObject = responseDocument.object()["domain"].toObject(); const QString DOMAIN_NAME_KEY = "name"; const QString DOMAIN_OWNER_PLACES_KEY = "owner_places"; if (domainObject.contains(DOMAIN_NAME_KEY)) { _domainServerName = domainObject[DOMAIN_NAME_KEY].toString(); } else if (domainObject.contains(DOMAIN_OWNER_PLACES_KEY)) { QJsonArray ownerPlaces = domainObject[DOMAIN_OWNER_PLACES_KEY].toArray(); if (ownerPlaces.size() > 0) { _domainServerName = ownerPlaces[0].toObject()[DOMAIN_NAME_KEY].toString(); } } qDebug() << "This domain server's name is" << _domainServerName << "- updating address link."; emit domainAddressChanged(); } } void AppDelegate::changeDomainServerIndexPath(const QString& newPath) { if (!newPath.isEmpty()) { QString pathsJSON = "{\"paths\": { \"/\": { \"viewpoint\": \"%1\" }}}"; QNetworkRequest settingsRequest(GlobalData::getInstance().getDomainServerBaseUrl() + "/settings.json"); settingsRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QNetworkReply* settingsReply = _manager->post(settingsRequest, pathsJSON.arg(newPath).toLocal8Bit()); connect(settingsReply, &QNetworkReply::finished, this, &AppDelegate::handleChangeIndexPathResponse); } } void AppDelegate::handleChangeIndexPathResponse() { QNetworkReply* reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { qDebug() << "Successfully changed index path in domain-server."; emit indexPathChangeResponse(true); } else { qDebug() << "Error changing domain-server index path-" << reply->errorString(); emit indexPathChangeResponse(false); } } void AppDelegate::downloadContentSet(const QUrl& contentSetURL) { // make sure this link was an svo if (contentSetURL.path().endsWith(".svo")) { // setup a request for this content set QNetworkRequest contentRequest(contentSetURL); QNetworkReply* contentReply = _manager->get(contentRequest); connect(contentReply, &QNetworkReply::finished, this, &AppDelegate::handleContentSetDownloadFinished); } } void AppDelegate::handleContentSetDownloadFinished() { QNetworkReply* reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { QString modelFilename = GlobalData::getInstance().getClientsResourcesPath() + "models.svo"; // write the model file QFile modelFile(modelFilename); modelFile.open(QIODevice::WriteOnly); // stop the base assignment clients before we try to write the new content toggleAssignmentClientMonitor(false); if (modelFile.write(reply->readAll()) == -1) { qDebug() << "Error writing content set to" << modelFilename; modelFile.close(); toggleAssignmentClientMonitor(true); } else { qDebug() << "Wrote new content set to" << modelFilename; modelFile.close(); // restart the assignment-client toggleAssignmentClientMonitor(true); emit contentSetDownloadResponse(true); // did we have a path in the query? // if so when we need to set the DS index path to that path QUrlQuery svoQuery(reply->url().query()); changeDomainServerIndexPath(svoQuery.queryItemValue("path")); emit domainAddressChanged(); return; } } // if we failed we need to emit our signal with a fail emit contentSetDownloadResponse(false); emit domainAddressChanged(); } void AppDelegate::onFileSuccessfullyInstalled(const QUrl& url) { if (url == GlobalData::getInstance().getRequirementsURL()) { _qtReady = true; } else if (url == GlobalData::getInstance().getAssignmentClientURL()) { _acReady = true; } else if (url == GlobalData::getInstance().getDomainServerURL()) { _dsReady = true; } else if (url == GlobalData::getInstance().getDomainServerResourcesURL()) { _dsResourcesReady = true; } if (_qtReady && _acReady && _dsReady && _dsResourcesReady) { _window->setRequirementsLastChecked(QDateTime::currentDateTime().toString()); _window->show(); toggleStack(true); } } void AppDelegate::createExecutablePath() { QDir launchDir(GlobalData::getInstance().getClientsLaunchPath()); QDir resourcesDir(GlobalData::getInstance().getClientsResourcesPath()); QDir logsDir(GlobalData::getInstance().getLogsPath()); if (!launchDir.exists()) { if (QDir().mkpath(launchDir.absolutePath())) { qDebug() << "Successfully created directory: " << launchDir.absolutePath(); } else { qCritical() << "Failed to create directory: " << launchDir.absolutePath(); } } if (!resourcesDir.exists()) { if (QDir().mkpath(resourcesDir.absolutePath())) { qDebug() << "Successfully created directory: " << resourcesDir.absolutePath(); } else { qCritical() << "Failed to create directory: " << resourcesDir.absolutePath(); } } if (!logsDir.exists()) { if (QDir().mkpath(logsDir.absolutePath())) { qDebug() << "Successfully created directory: " << logsDir.absolutePath(); } else { qCritical() << "Failed to create directory: " << logsDir.absolutePath(); } } } void AppDelegate::downloadLatestExecutablesAndRequirements() { // Check if Qt is already installed if (GlobalData::getInstance().getPlatform() == "mac") { if (QDir(GlobalData::getInstance().getClientsLaunchPath() + "QtCore.framework").exists()) { _qtReady = true; } } else if (GlobalData::getInstance().getPlatform() == "win") { if (QFileInfo(GlobalData::getInstance().getClientsLaunchPath() + "Qt5Core.dll").exists()) { _qtReady = true; } } else { // linux if (QFileInfo(GlobalData::getInstance().getClientsLaunchPath() + "libQt5Core.so.5").exists()) { _qtReady = true; } } QFile reqZipFile(GlobalData::getInstance().getRequirementsZipPath()); QByteArray reqZipData; if (reqZipFile.open(QIODevice::ReadOnly)) { reqZipData = reqZipFile.readAll(); reqZipFile.close(); } QFile resZipFile(GlobalData::getInstance().getDomainServerResourcesZipPath()); QByteArray resZipData; if (resZipFile.open(QIODevice::ReadOnly)) { resZipData = resZipFile.readAll(); resZipFile.close(); } QDir resourcesDir(GlobalData::getInstance().getClientsResourcesPath()); if (!(resourcesDir.entryInfoList(QDir::AllEntries).size() < 3)) { _dsResourcesReady = true; } // if the user has set hifiBuildDirectory, don't attempt to download the domain-server or assignement-client if (GlobalData::getInstance().isGetHifiBuildDirectorySet()) { _dsReady = true; _acReady = true; } else { QByteArray dsData; QFile dsFile(GlobalData::getInstance().getDomainServerExecutablePath()); if (dsFile.open(QIODevice::ReadOnly)) { dsData = dsFile.readAll(); dsFile.close(); } QByteArray acData; QFile acFile(GlobalData::getInstance().getAssignmentClientExecutablePath()); if (acFile.open(QIODevice::ReadOnly)) { acData = acFile.readAll(); acFile.close(); } QNetworkRequest acReq(QUrl(GlobalData::getInstance().getAssignmentClientMD5URL())); QNetworkReply* acReply = _manager->get(acReq); QEventLoop acLoop; connect(acReply, SIGNAL(finished()), &acLoop, SLOT(quit())); acLoop.exec(); QByteArray acMd5Data = acReply->readAll().trimmed(); if (GlobalData::getInstance().getPlatform() == "win") { // fix for reading the MD5 hash from Windows-generated // binary data of the MD5 hash QTextStream stream(acMd5Data); stream >> acMd5Data; } // fix for Mac and Linux network accessibility if (acMd5Data.size() == 0) { // network is not accessible qDebug() << "Could not connect to the internet."; _window->show(); return; } qDebug() << "AC MD5: " << acMd5Data; if (acMd5Data.toLower() == QCryptographicHash::hash(acData, QCryptographicHash::Md5).toHex()) { _acReady = true; } QNetworkRequest dsReq(QUrl(GlobalData::getInstance().getDomainServerMD5URL())); QNetworkReply* dsReply = _manager->get(dsReq); QEventLoop dsLoop; connect(dsReply, SIGNAL(finished()), &dsLoop, SLOT(quit())); dsLoop.exec(); QByteArray dsMd5Data = dsReply->readAll().trimmed(); if (GlobalData::getInstance().getPlatform() == "win") { // fix for reading the MD5 hash from Windows generated // binary data of the MD5 hash QTextStream stream(dsMd5Data); stream >> dsMd5Data; } qDebug() << "DS MD5: " << dsMd5Data; if (dsMd5Data.toLower() == QCryptographicHash::hash(dsData, QCryptographicHash::Md5).toHex()) { _dsReady = true; } } if (_qtReady) { // check MD5 of requirements.zip only if Qt is found QNetworkRequest reqZipReq(QUrl(GlobalData::getInstance().getRequirementsMD5URL())); QNetworkReply* reqZipReply = _manager->get(reqZipReq); QEventLoop reqZipLoop; connect(reqZipReply, SIGNAL(finished()), &reqZipLoop, SLOT(quit())); reqZipLoop.exec(); QByteArray reqZipMd5Data = reqZipReply->readAll().trimmed(); if (GlobalData::getInstance().getPlatform() == "win") { // fix for reading the MD5 hash from Windows generated // binary data of the MD5 hash QTextStream stream(reqZipMd5Data); stream >> reqZipMd5Data; } qDebug() << "Requirements ZIP MD5: " << reqZipMd5Data; if (reqZipMd5Data.toLower() != QCryptographicHash::hash(reqZipData, QCryptographicHash::Md5).toHex()) { _qtReady = false; } } if (_dsResourcesReady) { // check MD5 of resources.zip only if Domain Server // resources are installed QNetworkRequest resZipReq(QUrl(GlobalData::getInstance().getDomainServerResourcesMD5URL())); QNetworkReply* resZipReply = _manager->get(resZipReq); QEventLoop resZipLoop; connect(resZipReply, SIGNAL(finished()), &resZipLoop, SLOT(quit())); resZipLoop.exec(); QByteArray resZipMd5Data = resZipReply->readAll().trimmed(); if (GlobalData::getInstance().getPlatform() == "win") { // fix for reading the MD5 hash from Windows generated // binary data of the MD5 hash QTextStream stream(resZipMd5Data); stream >> resZipMd5Data; } qDebug() << "Domain Server Resources ZIP MD5: " << resZipMd5Data; if (resZipMd5Data.toLower() != QCryptographicHash::hash(resZipData, QCryptographicHash::Md5).toHex()) { _dsResourcesReady = false; } } DownloadManager* downloadManager = 0; if (!_qtReady || !_acReady || !_dsReady || !_dsResourcesReady) { // initialise DownloadManager downloadManager = new DownloadManager(_manager); downloadManager->setWindowModality(Qt::ApplicationModal); connect(downloadManager, SIGNAL(fileSuccessfullyInstalled(QUrl)), SLOT(onFileSuccessfullyInstalled(QUrl))); downloadManager->show(); } else { _window->setRequirementsLastChecked(QDateTime::currentDateTime().toString()); _window->show(); toggleStack(true); } if (!_qtReady) { downloadManager->downloadFile(GlobalData::getInstance().getRequirementsURL()); } if (!_acReady) { downloadManager->downloadFile(GlobalData::getInstance().getAssignmentClientURL()); } if (!_dsReady) { downloadManager->downloadFile(GlobalData::getInstance().getDomainServerURL()); } if (!_dsResourcesReady) { downloadManager->downloadFile(GlobalData::getInstance().getDomainServerResourcesURL()); } } void AppDelegate::checkVersion() { QNetworkRequest latestVersionRequest((QUrl(CHECK_BUILDS_URL))); latestVersionRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); latestVersionRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); QNetworkReply* reply = _manager->get(latestVersionRequest); connect(reply, &QNetworkReply::finished, this, &AppDelegate::parseVersionXml); _checkVersionTimer.setInterval(VERSION_CHECK_INTERVAL_MS); _checkVersionTimer.start(); } struct VersionInformation { QString version; QUrl downloadUrl; QString timeStamp; QString releaseNotes; }; void AppDelegate::parseVersionXml() { #ifdef Q_OS_WIN32 QString operatingSystem("windows"); #endif #ifdef Q_OS_MAC QString operatingSystem("mac"); #endif #ifdef Q_OS_LINUX QString operatingSystem("ubuntu"); #endif QNetworkReply* sender = qobject_cast(QObject::sender()); QXmlStreamReader xml(sender); QHash projectVersions; while (!xml.atEnd() && !xml.hasError()) { if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "project") { QString projectName = ""; foreach(const QXmlStreamAttribute &attr, xml.attributes()) { if (attr.name().toString() == "name") { projectName = attr.value().toString(); break; } } while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "project")) { if (projectName != "") { if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "platform") { QString platformName = ""; foreach(const QXmlStreamAttribute &attr, xml.attributes()) { if (attr.name().toString() == "name") { platformName = attr.value().toString(); break; } } int latestVersion = 0; VersionInformation latestVersionInformation; while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "platform")) { if (platformName == operatingSystem) { if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "build") { VersionInformation buildVersionInformation; while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "build")) { if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "version") { xml.readNext(); buildVersionInformation.version = xml.text().toString(); } if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "url") { xml.readNext(); buildVersionInformation.downloadUrl = QUrl(xml.text().toString()); } if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "timestamp") { xml.readNext(); buildVersionInformation.timeStamp = xml.text().toString(); } if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "note") { xml.readNext(); if (buildVersionInformation.releaseNotes != "") { buildVersionInformation.releaseNotes += "\n"; } buildVersionInformation.releaseNotes += xml.text().toString(); } xml.readNext(); } if (latestVersion < buildVersionInformation.version.toInt()) { latestVersionInformation = buildVersionInformation; latestVersion = buildVersionInformation.version.toInt(); } } } xml.readNext(); } if (latestVersion>0) { projectVersions[projectName] = latestVersionInformation; } } } xml.readNext(); } } xml.readNext(); } #ifdef WANT_DEBUG qDebug() << "parsed projects for OS" << operatingSystem; QHashIterator projectVersion(projectVersions); while (projectVersion.hasNext()) { projectVersion.next(); qDebug() << "project:" << projectVersion.key(); qDebug() << "version:" << projectVersion.value().version; qDebug() << "downloadUrl:" << projectVersion.value().downloadUrl.toString(); qDebug() << "timeStamp:" << projectVersion.value().timeStamp; qDebug() << "releaseNotes:" << projectVersion.value().releaseNotes; } #endif if (projectVersions.contains("stackmanager")) { VersionInformation latestVersion = projectVersions["stackmanager"]; if (QCoreApplication::applicationVersion() != latestVersion.version && QCoreApplication::applicationVersion() != "dev") { _window->setUpdateNotification("There is an update available. Please download and install version " + latestVersion.version + "."); _window->update(); } } sender->deleteLater(); }