Files
pilotclient/src/blackcore/application.cpp
Klaus Basan f73ee87e13 refs #882, support redirect transparently in CApplication
* SSL client certificate functionality used from utils
* support for max redirects

Remark:
QNetworkRequest::FollowRedirectsAttribute would allow auto redirect, but we use our approach as it gives us better control
2017-02-24 15:21:35 +00:00

1171 lines
42 KiB
C++

/* Copyright (C) 2016
* swift project Community / Contributors
*
* This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level
* directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project,
* including this file, may be copied, modified, propagated, or distributed except according to the terms
* contained in the LICENSE file.
*/
#include "blackconfig/buildconfig.h"
#include "blackcore/application.h"
#include "blackcore/context/contextapplication.h"
#include "blackcore/cookiemanager.h"
#include "blackcore/corefacade.h"
#include "blackcore/vatsim/networkvatlib.h"
#include "blackcore/registermetadata.h"
#include "blackcore/setupreader.h"
#include "blackcore/webdataservices.h"
#include "blackmisc/atomicfile.h"
#include "blackmisc/datacache.h"
#include "blackmisc/dbusserver.h"
#include "blackmisc/directoryutils.h"
#include "blackmisc/filelogger.h"
#include "blackmisc/logcategory.h"
#include "blackmisc/logcategorylist.h"
#include "blackmisc/loghandler.h"
#include "blackmisc/logmessage.h"
#include "blackmisc/logpattern.h"
#include "blackmisc/network/networkutils.h"
#include "blackmisc/registermetadata.h"
#include "blackmisc/settingscache.h"
#include "blackmisc/slot.h"
#include "blackmisc/stringutils.h"
#include "blackmisc/threadutils.h"
#include "blackmisc/verify.h"
#include <stdbool.h>
#include <stdio.h>
#include <QCoreApplication>
#include <QDateTime>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslSocket>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QThread>
#include <QTime>
#include <QTimer>
#include <QTranslator>
#include <QWriteLocker>
#include <Qt>
#include <QtGlobal>
#include <cstdlib>
#ifdef BLACK_USE_CRASHPAD
#include "crashpad/client/crashpad_client.h"
#include "crashpad/client/crash_report_database.h"
#include "crashpad/client/settings.h"
#endif
using namespace BlackConfig;
using namespace BlackMisc;
using namespace BlackMisc::Network;
using namespace BlackMisc::Aviation;
using namespace BlackMisc::Simulation;
using namespace BlackMisc::Weather;
using namespace BlackCore;
using namespace BlackCore::Context;
using namespace BlackCore::Vatsim;
using namespace BlackCore::Data;
using namespace BlackCore::Db;
using namespace crashpad;
BlackCore::CApplication *sApp = nullptr; // set by constructor
//! \private
static const QString &swiftDataRoot()
{
static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/org.swift-project/";
return path;
}
namespace BlackCore
{
CApplication::CApplication(const QString &applicationName, CApplicationInfo::Application application, bool init) :
m_cookieManager( {}, this), m_applicationName(applicationName), m_application(application), m_coreFacadeConfig(CCoreFacadeConfig::allEmpty())
{
Q_ASSERT_X(!sApp, Q_FUNC_INFO, "already initialized");
Q_ASSERT_X(QCoreApplication::instance(), Q_FUNC_INFO, "no application object");
// init skiped when called from CGuiApplication
if (init)
{
this->init(true);
}
}
void CApplication::init(bool withMetadata)
{
if (!sApp)
{
if (withMetadata) { CApplication::registerMetadata(); }
QCoreApplication::setApplicationName(this->m_applicationName);
QCoreApplication::setApplicationVersion(CVersion::version());
this->setObjectName(this->m_applicationName);
const QString executable = QFileInfo(QCoreApplication::applicationFilePath()).fileName();
if (executable.startsWith("test"))
{
this->m_unitTest = true;
const QString tempPath(this->getTemporaryDirectory());
BlackMisc::setMockCacheRootDirectory(tempPath);
}
this->initParser();
this->initLogging();
//
// cmd line arguments not yet parsed here
//
// Translations
QFile file(":blackmisc/translations/blackmisc_i18n_de.qm");
CLogMessage(this).debug() << (file.exists() ? "Found translations in resources" : "No translations in resources");
QTranslator translator;
if (translator.load("blackmisc_i18n_de", ":blackmisc/translations/")) { CLogMessage(this).debug() << "Translator loaded"; }
QCoreApplication::instance()->installTranslator(&translator);
// Init network
this->m_cookieManager.setParent(&this->m_accessManager);
this->m_accessManager.setCookieJar(&this->m_cookieManager);
connect(&this->m_accessManager, &QNetworkAccessManager::networkAccessibleChanged, this, &CApplication::ps_networkAccessibleChanged);
// global setup
sApp = this;
this->m_setupReader.reset(new CSetupReader(this));
connect(this->m_setupReader.data(), &CSetupReader::setupHandlingCompleted, this, &CApplication::ps_setupHandlingCompleted);
connect(this->m_setupReader.data(), &CSetupReader::updateInfoAvailable, this, &CApplication::updateInfoAvailable);
this->m_parser.addOptions(this->m_setupReader->getCmdLineOptions());
// startup done
connect(this, &CApplication::startUpCompleted, this, &CApplication::ps_startupCompleted);
// notify when app goes down
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &CApplication::gracefulShutdown);
}
}
int CApplication::exec()
{
Q_ASSERT_X(instance(), Q_FUNC_INFO, "missing application");
CApplicationInfoList apps = getRunningApplications();
apps.push_back(instance()->getApplicationInfo());
bool ok = CFileUtils::writeStringToLockedFile(apps.toJsonString(), swiftDataRoot() + "apps.json");
if (!ok) { CLogMessage(static_cast<CApplication *>(nullptr)).error("Failed to write to application list file"); }
return QCoreApplication::exec();
}
CApplication::~CApplication()
{
this->gracefulShutdown();
}
CApplicationInfo CApplication::getApplicationInfo() const
{
CApplicationInfo::ApplicationMode mode;
if (isRunningInDeveloperEnvironment()) { mode |= CApplicationInfo::Developer; }
if (CBuildConfig::isBetaTest()) { mode |= CApplicationInfo::BetaTest; }
return { getSwiftApplication(), mode, QCoreApplication::applicationFilePath(), CVersion::version(), CProcessInfo::currentProcess() };
}
CApplicationInfoList CApplication::getRunningApplications()
{
CApplicationInfoList apps;
apps.convertFromJsonNoThrow(CFileUtils::readLockedFileToString(swiftDataRoot() + "apps.json"), {}, {});
apps.removeIf([](const CApplicationInfo & info) { return !info.processInfo().exists(); });
return apps;
}
bool CApplication::isAlreadyRunning() const
{
return getRunningApplications().containsBy([this](const CApplicationInfo & info) { return info.application() == getSwiftApplication(); });
}
const QString &CApplication::getApplicationNameAndVersion() const
{
static const QString s(QCoreApplication::instance()->applicationName() + " " + CVersion::version());
return s;
}
const QString &CApplication::getApplicationNameVersionBetaDev() const
{
static const QString s(QCoreApplication::instance()->applicationName() + " " + this->versionStringDevBetaInfo());
return s;
}
CApplicationInfo::Application CApplication::getSwiftApplication() const
{
if (this->isUnitTest()) { return CApplicationInfo::UnitTest; }
if (this->m_application != CApplicationInfo::Unknown) { return this->m_application; }
// if not set, guess
BLACK_VERIFY_X(false, Q_FUNC_INFO, "Missing application");
const QString a(QCoreApplication::instance()->applicationName().toLower());
if (a.contains("core")) { return CApplicationInfo::PilotClientCore; }
if (a.contains("launcher")) { return CApplicationInfo::Laucher; }
if (a.contains("gui")) { return CApplicationInfo::PilotClientGui; }
if (a.contains("data") || a.contains("mapping")) { return CApplicationInfo::MappingTool; }
return CApplicationInfo::Unknown;
}
bool CApplication::isUnitTest() const
{
return this->m_unitTest;
}
CGlobalSetup CApplication::getGlobalSetup() const
{
if (this->m_shutdown) { return CGlobalSetup(); }
const CSetupReader *r = this->m_setupReader.data();
if (!r) { return CGlobalSetup(); }
return r->getSetup();
}
CUpdateInfo CApplication::getUpdateInfo() const
{
if (this->m_shutdown) { return CUpdateInfo(); }
const CSetupReader *r = this->m_setupReader.data();
if (!r) { return CUpdateInfo(); }
return r->getUpdateInfo();
}
bool CApplication::start()
{
// parse if needed, parsing contains its own error handling
if (!this->m_parsed)
{
bool s = this->parse();
if (!s) { return false; }
}
// parsing itself is done
CStatusMessageList msgs;
do
{
// clear cache?
if (this->isSetOrTrue(this->m_cmdClearCache))
{
const QStringList files(CApplication::clearCaches());
msgs.push_back(
CLogMessage(this).debug() << "Cleared cache, " << files.size() << " files"
);
}
if (this->m_startSetupReader && !this->m_setupReader->isSetupAvailable())
{
msgs = this->requestReloadOfSetupAndVersion();
if (msgs.isSuccess())
{
msgs.push_back(this->waitForSetup());
}
if (msgs.isFailure()) { break; }
}
// start hookin
msgs.push_back(this->startHookIn());
if (msgs.isFailure()) { break; }
// trigger loading and saving of settings in appropriate scenarios
if (this->m_coreFacadeConfig.getModeApplication() != CCoreFacadeConfig::Remote)
{
// facade running here locally
msgs.push_back(CSettingsCache::instance()->loadFromStore());
if (msgs.isFailure()) { break; }
// Settings are distributed via DBus. So only one application is responsible for saving. `enableLocalSave()` means
// "this is the application responsible for saving". If swiftgui requests a setting to be saved, it is sent to swiftcore and saved by swiftcore.
CSettingsCache::instance()->enableLocalSave();
// From this moment on, we have settings, so enable crash handler.
msgs.push_back(this->initCrashHandler());
}
}
while (false);
// terminate with failures, otherwise log messages
if (msgs.isFailure())
{
this->cmdLineErrorMessage(msgs);
return false;
}
else if (!msgs.isEmpty())
{
CLogMessage::preformatted(msgs);
}
this->m_started = true;
return this->m_started;
}
CStatusMessageList CApplication::waitForSetup()
{
if (!this->m_setupReader) { return CStatusMessage(this).error("No setup reader"); }
if (!this->m_setupReader->isSetupAvailable())
{
QEventLoop eventLoop;
QTimer::singleShot(5000, &eventLoop, &QEventLoop::quit);
connect(this, &CApplication::setupHandlingCompleted, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
}
// setup handling completed with success or failure, or we run into time out
if (this->m_setupReader->isSetupAvailable()) { return CStatusMessage(this).info("Setup available"); }
CStatusMessageList msgs(CStatusMessage(this).error("Setup not available, setup reading failed or timed out."));
if (this->m_setupReader->getLastSetupReadErrorMessages().hasErrorMessages())
{
msgs.push_back(this->m_setupReader->getLastSetupReadErrorMessages());
}
if (this->m_setupReader->hasCmdLineBootstrapUrl())
{
msgs.push_back(CStatusMessage(this).info("Check cmd line argument '%1'") << this->m_setupReader->getCmdLineBootstrapUrl());
}
return msgs;
}
bool CApplication::isSetupAvailable() const
{
if (this->m_shutdown || !this->m_setupReader) { return false; }
return this->m_setupReader->isSetupAvailable();
}
CStatusMessageList CApplication::requestReloadOfSetupAndVersion()
{
if (!this->m_shutdown)
{
Q_ASSERT_X(this->m_setupReader, Q_FUNC_INFO, "Missing reader");
Q_ASSERT_X(this->m_parsed, Q_FUNC_INFO, "Not yet parsed");
return this->m_setupReader->asyncLoad();
}
else
{
return CStatusMessage(this).error("No reader for setup/version");
}
}
bool CApplication::hasWebDataServices() const
{
return this->m_webDataServices;
}
CWebDataServices *CApplication::getWebDataServices() const
{
// use hasWebDataServices() to test if services are available
Q_ASSERT_X(this->m_webDataServices, Q_FUNC_INFO, "Missing web data services, use hasWebDataServices to test if existing");
return this->m_webDataServices.data();
}
bool CApplication::isApplicationThread() const
{
return CThreadUtils::isCurrentThreadApplicationThread();
}
const QString &CApplication::versionStringDevBetaInfo() const
{
if (isRunningInDeveloperEnvironment() && CBuildConfig::isBetaTest())
{
static const QString s(CVersion::version() + " [DEV, BETA]");
return s;
}
if (isRunningInDeveloperEnvironment())
{
static const QString s(CVersion::version() + " [DEV]");
return s;
}
if (CBuildConfig::isBetaTest())
{
static const QString s(CVersion::version() + " [BETA]");
return s;
}
return CVersion::version();
}
const QString &CApplication::swiftVersionString() const
{
static const QString s(QString("swift %1").arg(versionStringDevBetaInfo()));
return s;
}
const char *CApplication::swiftVersionChar()
{
static const QByteArray a(swiftVersionString().toUtf8());
return a.constData();
}
bool CApplication::initIsRunningInDeveloperEnvironment() const
{
if (!CBuildConfig::canRunInDeveloperEnvironment()) { return false; }
if (this->m_unitTest) { return true; }
if (this->m_parser.isSet(this->m_cmdDevelopment))
{
return this->isSetOrTrue(this->m_cmdDevelopment);
}
else if (this->isSetupAvailable())
{
// assume value from setup
return this->getGlobalSetup().isDevelopment();
}
return false;
}
void CApplication::setSignalStartupAutomatically(bool enabled)
{
this->m_signalStartup = enabled;
}
QString CApplication::getEnvironmentInfoString(const QString &separator) const
{
QString env("Beta: ");
env.append(boolToYesNo(CBuildConfig::isBetaTest()));
env = env.append(" dev.env,: ").append(boolToYesNo(isRunningInDeveloperEnvironment()));
env = env.append(separator);
env.append("Windows: ").append(boolToYesNo(CBuildConfig::isRunningOnWindowsNtPlatform()));
return env;
}
bool CApplication::hasUnsavedSettings() const
{
return !this->getAllUnsavedSettings().isEmpty();
}
void CApplication::setSettingsAutoSave(bool autoSave)
{
this->m_autoSaveSettings = autoSave;
}
QStringList CApplication::getAllUnsavedSettings() const
{
return CSettingsCache::instance()->getAllUnsavedKeys();
}
CStatusMessage CApplication::saveSettingsByKey(const QStringList &keys)
{
if (keys.isEmpty()) { return CStatusMessage(); }
if (this->supportsContexts())
{
return this->getIContextApplication()->saveSettingsByKey(keys);
}
else
{
return CSettingsCache::instance()->saveToStore(keys);
}
}
QString CApplication::getTemporaryDirectory() const
{
static QTemporaryDir tempDir;
if (tempDir.isValid()) { return tempDir.path(); }
return QDir::tempPath();
}
QString CApplication::getInfoString(const QString &separator) const
{
QString str(CVersion::version());
str = str.append(" ").append(CBuildConfig::isReleaseBuild() ? "Release build" : "Debug build");
str = str.append(separator);
str = str.append(getEnvironmentInfoString(separator));
str = str.append(separator);
str.append(CBuildConfig::compiledWithInfo(false));
return str;
}
QNetworkReply *CApplication::getFromNetwork(const CUrl &url, const CSlot<void(QNetworkReply *)> &callback, int maxRedirects)
{
return httpRequestImpl(url.toNetworkRequest(), callback, maxRedirects, [ ](QNetworkAccessManager & nam, const QNetworkRequest & request) { return nam.get(request); });
}
QNetworkReply *CApplication::getFromNetwork(const QNetworkRequest &request, const CSlot<void(QNetworkReply *)> &callback, int maxRedirects)
{
return httpRequestImpl(request, callback, maxRedirects, [ ](QNetworkAccessManager & nam, const QNetworkRequest & request) { return nam.get(request); });
}
QNetworkReply *CApplication::postToNetwork(const QNetworkRequest &request, const QByteArray &data, const CSlot<void(QNetworkReply *)> &callback)
{
return httpRequestImpl(request, callback, -1, [ data ](QNetworkAccessManager & nam, const QNetworkRequest & request) { return nam.post(request, data); });
}
QNetworkReply *CApplication::postToNetwork(const QNetworkRequest &request, QHttpMultiPart *multiPart, const CSlot<void(QNetworkReply *)> &callback)
{
if (!this->isNetworkConnectedAndAccessible()) { return nullptr; }
if (QThread::currentThread() != this->m_accessManager.thread())
{
multiPart->moveToThread(this->m_accessManager.thread());
}
return httpRequestImpl(request, callback, -1, [ this, multiPart ](QNetworkAccessManager & nam, const QNetworkRequest & request)
{
QNetworkReply *reply = nam.post(request, multiPart);
Q_ASSERT(reply);
multiPart->setParent(reply);
return reply;
});
}
QNetworkReply *CApplication::headerFromNetwork(const CUrl &url, const CSlot<void (QNetworkReply *)> &callback, int maxRedirects)
{
return httpRequestImpl(url.toNetworkRequest(), callback, maxRedirects, [ ](QNetworkAccessManager & nam, const QNetworkRequest & request) { return nam.head(request); });
}
QNetworkReply *CApplication::headerFromNetwork(const QNetworkRequest &request, const CSlot<void (QNetworkReply *)> &callback, int maxRedirects)
{
return httpRequestImpl(request, callback, maxRedirects, [ ](QNetworkAccessManager & nam, const QNetworkRequest & request) { return nam.head(request); });
}
void CApplication::deleteAllCookies()
{
this->m_cookieManager.deleteAllCookies();
}
bool CApplication::isNetworkAccessible() const
{
return this->m_accessManager.networkAccessible() == QNetworkAccessManager::Accessible;
}
bool CApplication::isNetworkConnected() const
{
static bool con = CNetworkUtils::hasConnectedInterface();
return con;
}
bool CApplication::isNetworkConnectedAndAccessible() const
{
return this->isNetworkConnected() && this->isNetworkAccessible();
}
void CApplication::exit(int retcode)
{
if (instance())
{
instance()->gracefulShutdown();
}
// when the event loop is not running, this does nothing
QCoreApplication::exit(retcode);
}
QStringList CApplication::arguments()
{
return QCoreApplication::arguments();
}
void CApplication::processEventsFor(int milliseconds)
{
QEventLoop eventLoop;
QTimer::singleShot(milliseconds, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
}
CStatusMessageList CApplication::useContexts(const CCoreFacadeConfig &coreConfig)
{
Q_ASSERT_X(this->m_parsed, Q_FUNC_INFO, "Call this function after parsing");
this->m_useContexts = true;
this->m_coreFacadeConfig = coreConfig;
// if not yet initialized, init web data services
if (!this->m_useWebData)
{
const CStatusMessageList msgs = this->useWebDataServices(CWebReaderFlags::AllReaders, CDatabaseReaderConfigList::forPilotClient());
if (msgs.hasErrorMessages()) { return msgs; }
}
return this->startCoreFacadeAndWebDataServices(); // will do nothing if setup is not yet loaded
}
CStatusMessageList CApplication::useWebDataServices(const CWebReaderFlags::WebReader webReaders, const CDatabaseReaderConfigList &dbReaderConfig)
{
Q_ASSERT_X(this->m_webDataServices.isNull(), Q_FUNC_INFO, "Services already started");
BLACK_VERIFY_X(QSslSocket::supportsSsl(), Q_FUNC_INFO, "No SSL");
if (!QSslSocket::supportsSsl())
{
return CStatusMessage(this).error("No SSL supported, can`t be used");
}
this->m_webReadersUsed = webReaders;
this->m_dbReaderConfig = dbReaderConfig;
this->m_useWebData = true;
return this->startWebDataServices();
}
CStatusMessageList CApplication::startCoreFacadeAndWebDataServices()
{
Q_ASSERT_X(this->m_parsed, Q_FUNC_INFO, "Call this function after parsing");
if (!this->m_useContexts) { return CStatusMessage(this).error("No need to start core facade"); } // we do not use context, so no need to startup
if (!this->m_setupReader || !this->m_setupReader->isSetupAvailable()) { return CStatusMessage(this).error("No setup reader or setup available"); }
Q_ASSERT_X(this->m_coreFacade.isNull(), Q_FUNC_INFO, "Cannot alter facade");
Q_ASSERT_X(this->m_setupReader, Q_FUNC_INFO, "No facade without setup possible");
Q_ASSERT_X(this->m_useWebData, Q_FUNC_INFO, "Need web data services");
this->startWebDataServices();
const CStatusMessageList msgs(CStatusMessage(this).info("Will start core facade now"));
this->m_coreFacade.reset(new CCoreFacade(this->m_coreFacadeConfig));
emit this->coreFacadeStarted();
return msgs;
}
CStatusMessageList CApplication::startWebDataServices()
{
Q_ASSERT_X(this->m_parsed, Q_FUNC_INFO, "Call this function after parsing");
if (!this->m_useWebData) { return CStatusMessage(this).warning("No need to start web data services"); }
if (!this->m_setupReader || !this->m_setupReader->isSetupAvailable()) { return CStatusMessage(this).error("No setup reader or setup available"); }
Q_ASSERT_X(this->m_setupReader, Q_FUNC_INFO, "No web data services without setup possible");
CStatusMessageList msgs;
if (!this->m_webDataServices)
{
msgs.push_back(CStatusMessage(this).info("Will start web data services now"));
this->m_webDataServices.reset(
new CWebDataServices(this->m_webReadersUsed, this->m_dbReaderConfig, {}, this)
);
}
else
{
msgs.push_back(CStatusMessage(this).info("Web data services already running"));
}
emit webDataServicesStarted(true);
return msgs;
}
void CApplication::initLogging()
{
CLogHandler::instance()->install(); // make sure we have a log handler!
// File logger
this->m_fileLogger.reset(new CFileLogger(executable(), CDirectoryUtils::getLogDirectory()));
this->m_fileLogger->changeLogPattern(CLogPattern().withSeverityAtOrAbove(CStatusMessage::SeverityDebug));
}
void CApplication::initParser()
{
this->m_parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
this->m_parser.setApplicationDescription(m_applicationName);
this->m_cmdHelp = this->m_parser.addHelpOption();
this->m_cmdVersion = this->m_parser.addVersionOption();
// dev. system
this->m_cmdDevelopment = QCommandLineOption({ "dev", "development" },
QCoreApplication::translate("application", "Dev. system features?"),
"development");
this->addParserOption(this->m_cmdDevelopment);
// can read a local bootsrap file
this->m_cmdSharedDir = QCommandLineOption({ "shared", "shareddir" },
QCoreApplication::translate("application", "Local shared directory."),
"shared");
this->addParserOption(this->m_cmdSharedDir);
// reset caches upfront
this->m_cmdClearCache = QCommandLineOption({ "ccache", "clearcache" },
QCoreApplication::translate("application", "Clear (reset) the caches."),
"clearcache");
this->addParserOption(this->m_cmdClearCache);
}
bool CApplication::isSetOrTrue(const QCommandLineOption &option) const
{
if (!this->m_parser.isSet(option)) { return false; }
// explicit value
const QString v(this->m_parser.value(option).trimmed());
if (v.isEmpty()) { return true; } // just flag
if (v.startsWith("-")) { return true; } // just flag, because value is already next parameter
return stringToBool(v);
}
void CApplication::registerMetadata()
{
BlackMisc::registerMetadata();
BlackCore::registerMetadata();
}
QStringList CApplication::clearCaches()
{
const QStringList files(CDataCache::instance()->enumerateStore());
CDataCache::instance()->clearAllValues();
return files;
}
void CApplication::gracefulShutdown()
{
if (this->m_shutdown) { return; }
this->m_shutdown = true;
// save settings (but only when application was really alive)
CStatusMessage m;
if (this->m_parsed)
{
if (this->supportsContexts() && this->m_autoSaveSettings)
{
// this will eventually also call saveToStore
m = this->getIContextApplication()->saveSettings();
}
else
{
m = CSettingsCache::instance()->saveToStore();
}
CLogMessage(getLogCategories()).preformatted(m);
}
// from here on we really rip appart the application object
// and it should no longer be used
sApp = nullptr;
disconnect(this);
if (this->supportsContexts())
{
// clean up facade
this->m_coreFacade->gracefulShutdown();
this->m_coreFacade.reset();
}
if (this->m_webDataServices)
{
this->m_webDataServices->gracefulShutdown();
this->m_webDataServices.reset();
}
if (this->m_setupReader)
{
this->m_setupReader->gracefulShutdown();
this->m_setupReader.reset();
}
this->m_fileLogger->close();
}
void CApplication::ps_setupHandlingCompleted(bool available)
{
if (available)
{
// start follow ups when setup is avaialable
const CStatusMessageList msgs = this->asyncWebAndContextStart();
this->m_started = msgs.isSuccess();
}
emit this->setupHandlingCompleted(available);
if (this->m_signalStartup)
{
emit this->startUpCompleted(this->m_started);
}
}
void CApplication::ps_startupCompleted()
{
// void
}
void CApplication::ps_networkAccessibleChanged(QNetworkAccessManager::NetworkAccessibility accessible)
{
switch (accessible)
{
case QNetworkAccessManager::Accessible:
CLogMessage(this).info("Network is accessible");
break;
case QNetworkAccessManager::NotAccessible:
CLogMessage(this).error("Network not accessible");
break;
default:
CLogMessage(this).warning("Network accessibility unknown");
break;
}
}
CStatusMessageList CApplication::asyncWebAndContextStart()
{
if (this->m_started) { return CStatusMessage(this).info("Already started "); }
// follow up startups
CStatusMessageList msgs = this->startWebDataServices();
if (msgs.isFailure()) return msgs;
msgs.push_back(this->startCoreFacadeAndWebDataServices());
return msgs;
}
void CApplication::severeStartupProblem(const CStatusMessage &message)
{
CLogMessage::preformatted(message);
this->cmdLineErrorMessage(message.getMessage());
this->exit(EXIT_FAILURE);
// if I get here the event loop was not yet running
std::exit(EXIT_FAILURE);
}
CApplication *BlackCore::CApplication::instance()
{
return sApp;
}
const QString &CApplication::executable()
{
static const QString e(QFileInfo(QCoreApplication::applicationFilePath()).completeBaseName());
return e;
}
const BlackMisc::CLogCategoryList &CApplication::getLogCategories()
{
static const CLogCategoryList l({ CLogCategory("swift.application"), CLogCategory("swift." + executable())});
return l;
}
// ---------------------------------------------------------------------------------
// Parsing
// ---------------------------------------------------------------------------------
bool CApplication::addParserOption(const QCommandLineOption &option)
{
return this->m_parser.addOption(option);
}
bool CApplication::addParserOptions(const QList<QCommandLineOption> &options)
{
return this->m_parser.addOptions(options);
}
void CApplication::addDBusAddressOption()
{
this->m_cmdDBusAddress = QCommandLineOption({ "dbus", "dbusaddress" },
QCoreApplication::translate("application", "DBus address (session, system, P2P IP e.g. 192.168.23.5)"),
"dbusaddress");
this->addParserOption(this->m_cmdDBusAddress);
}
void CApplication::addVatlibOptions()
{
this->addParserOptions(CNetworkVatlib::getCmdLineOptions());
}
QString CApplication::getCmdDBusAddressValue() const
{
if (this->isParserOptionSet(this->m_cmdDBusAddress))
{
const QString v(this->getParserValue(m_cmdDBusAddress));
const QString dBusAddress(CDBusServer::normalizeAddress(v));
return dBusAddress;
}
else
{
return "";
}
}
QString CApplication::getCmdSwiftPrivateSharedDir() const
{
return this->m_parser.value(this->m_cmdSharedDir);
}
bool CApplication::isParserOptionSet(const QString &option) const
{
return this->m_parser.isSet(option);
}
bool CApplication::isParserOptionSet(const QCommandLineOption &option) const
{
return this->m_parser.isSet(option);
}
QString CApplication::getParserValue(const QString &option) const
{
return this->m_parser.value(option).trimmed();
}
QString CApplication::getParserValue(const QCommandLineOption &option) const
{
return this->m_parser.value(option).trimmed();
}
bool CApplication::parse()
{
if (this->m_parsed) { return m_parsed; }
if (CBuildConfig::isLifetimeExpired())
{
this->cmdLineErrorMessage("Program exired " + CBuildConfig::getEol().toString());
return false;
}
// we call parse because we also want to display a GUI error message when applicable
const QStringList args(QCoreApplication::instance()->arguments());
if (!this->m_parser.parse(args))
{
this->cmdLineErrorMessage(this->m_parser.errorText());
return false;
}
// help/version
if (this->m_parser.isSet(this->m_cmdHelp))
{
// Important: parser help will already stop application
this->cmdLineHelpMessage();
return false;
}
if (this->m_parser.isSet(this->m_cmdVersion))
{
// Important: version will already stop application
this->cmdLineVersionMessage();
return false;
}
// dev.
this->m_devEnv = this->initIsRunningInDeveloperEnvironment();
// Hookin, other parsing
if (!this->parsingHookIn()) { return false; }
// setup reader
this->m_startSetupReader = this->m_setupReader->parseCmdLineArguments();
this->m_parsed = true;
return true;
}
void CApplication::cmdLineErrorMessage(const QString &errorMessage) const
{
fputs(qPrintable(errorMessage), stderr);
fputs("\n\n", stderr);
fputs(qPrintable(this->m_parser.helpText()), stderr);
}
void CApplication::cmdLineErrorMessage(const CStatusMessageList &msgs) const
{
if (msgs.isEmpty()) { return; }
if (!msgs.hasErrorMessages()) { return; }
return CApplication::cmdLineErrorMessage(
msgs.toFormattedQString(true)
);
}
void CApplication::cmdLineHelpMessage()
{
this->m_parser.showHelp(); // terminates
Q_UNREACHABLE();
}
void CApplication::cmdLineVersionMessage() const
{
printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), qPrintable(QCoreApplication::applicationVersion()));
}
// ---------------------------------------------------------------------------------
// Contexts
// ---------------------------------------------------------------------------------
bool CApplication::supportsContexts() const
{
if (this->m_shutdown) { return false; }
if (this->m_coreFacade.isNull()) { return false; }
if (!this->m_coreFacade->getIContextApplication()) { return false; }
return (!this->m_coreFacade->getIContextApplication()->isEmptyObject());
}
const IContextNetwork *CApplication::getIContextNetwork() const
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextNetwork();
}
const IContextAudio *CApplication::getIContextAudio() const
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextAudio();
}
const IContextApplication *CApplication::getIContextApplication() const
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextApplication();
}
const IContextOwnAircraft *CApplication::getIContextOwnAircraft() const
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextOwnAircraft();
}
const IContextSimulator *CApplication::getIContextSimulator() const
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextSimulator();
}
IContextNetwork *CApplication::getIContextNetwork()
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextNetwork();
}
IContextAudio *CApplication::getIContextAudio()
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextAudio();
}
IContextApplication *CApplication::getIContextApplication()
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextApplication();
}
IContextOwnAircraft *CApplication::getIContextOwnAircraft()
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextOwnAircraft();
}
IContextSimulator *CApplication::getIContextSimulator()
{
if (!supportsContexts()) { return nullptr; }
return this->m_coreFacade->getIContextSimulator();
}
// ---------------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------------
CUrlList CApplication::getVatsimMetarUrls() const
{
if (this->m_shutdown) { return CUrlList(); }
if (this->m_webDataServices)
{
const CUrlList urls(this->m_webDataServices->getVatsimMetarUrls());
if (!urls.empty()) { return urls; }
}
if (this->m_setupReader)
{
return this->m_setupReader->getSetup().getVatsimMetarsUrls();
}
return CUrlList();
}
CUrlList CApplication::getVatsimDataFileUrls() const
{
if (this->m_shutdown) { return CUrlList(); }
if (this->m_webDataServices)
{
const CUrlList urls(this->m_webDataServices->getVatsimDataFileUrls());
if (!urls.empty()) { return urls; }
}
if (this->m_setupReader)
{
return this->m_setupReader->getSetup().getVatsimDataFileUrls();
}
return CUrlList();
}
#ifdef BLACK_USE_CRASHPAD
base::FilePath qstringToFilePath(const QString &str)
{
# ifdef Q_OS_WIN
return base::FilePath(str.toStdWString());
# else
return base::FilePath(str.toStdString());
# endif
}
#endif
BlackMisc::CStatusMessageList CApplication::initCrashHandler()
{
#ifdef BLACK_USE_CRASHPAD
// No crash handling for unit tests
if (isUnitTest()) { return CStatusMessage(this).info("No crash handler for unit tests"); }
static const QString extension = CBuildConfig::isRunningOnWindowsNtPlatform() ? ".exe" : QString();
static const QString handler = CDirectoryUtils::applicationDirectoryPath() + "/" + "swift_crashpad_handler" + extension;
static const QString crashpadPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
"/org.swift-project/" +
CDirectoryUtils::normalizedApplicationDirectory() +
"/crashpad";
static const QString database = crashpadPath + "/database";
static const QString metrics = crashpadPath + "/metrics";
if (!QFileInfo::exists(handler))
{
return CStatusMessage(this).warning("%1 not found. Cannot init crash handler!") << handler;
}
CUrl serverUrl;
serverUrl = getGlobalSetup().getCrashReportServerUrl();
std::map<std::string, std::string> annotations;
// Caliper (mini-breakpad-server) annotations
annotations["prod"] = executable().toStdString();
annotations["ver"] = CVersion::version().toStdString();
m_crashReportDatabase = CrashReportDatabase::Initialize(qstringToFilePath(database));
auto settings = m_crashReportDatabase->GetSettings();
settings->SetUploadsEnabled(CBuildConfig::isReleaseBuild() && m_crashDumpUploadEnabled.getThreadLocal());
m_crashpadClient = std::make_unique<CrashpadClient>();
m_crashpadClient->StartHandler(qstringToFilePath(handler), qstringToFilePath(database), qstringToFilePath(metrics),
serverUrl.getFullUrl().toStdString(), annotations, {}, false, true);
return CStatusMessage(this).info("Using crash handler");
#else
return CStatusMessage(this).info("Not using crash handler");
#endif
}
void CApplication::crashDumpUploadEnabledChanged()
{
#ifdef BLACK_USE_CRASHPAD
if (!m_crashReportDatabase) { return; }
auto settings = m_crashReportDatabase->GetSettings();
settings->SetUploadsEnabled(CBuildConfig::isReleaseBuild() && m_crashDumpUploadEnabled.getThreadLocal());
#endif
}
QNetworkReply *CApplication::httpRequestImpl(const QNetworkRequest &request, const BlackMisc::CSlot<void (QNetworkReply *)> &callback, int maxRedirects, std::function<QNetworkReply *(QNetworkAccessManager &, const QNetworkRequest &)> requestOrPostMethod)
{
if (this->m_shutdown) { return nullptr; }
if (!this->isNetworkConnectedAndAccessible()) { return nullptr; }
QWriteLocker locker(&m_accessManagerLock);
Q_ASSERT_X(QCoreApplication::instance()->thread() == m_accessManager.thread(), Q_FUNC_INFO, "Network manager supposed to be in main thread");
if (QThread::currentThread() != this->m_accessManager.thread())
{
QTimer::singleShot(0, this, std::bind(&CApplication::httpRequestImpl, this, request, callback, maxRedirects, requestOrPostMethod));
return nullptr; // not yet started
}
Q_ASSERT_X(QThread::currentThread() == m_accessManager.thread(), Q_FUNC_INFO, "Network manager thread mismatch");
QNetworkRequest copiedRequest(request); // no QObject
CNetworkUtils::ignoreSslVerification(copiedRequest);
CNetworkUtils::setSwiftUserAgent(copiedRequest);
// If URL is one of the shared urls, add swift client SSL certificate
CNetworkUtils::setSwiftClientSslCertificate(copiedRequest, getGlobalSetup().getSwiftSharedUrls());
QNetworkReply *reply = requestOrPostMethod(this->m_accessManager, copiedRequest);
reply->setProperty("started", QVariant(QDateTime::currentMSecsSinceEpoch()));
if (callback)
{
connect(reply, &QNetworkReply::finished, callback.object(), [ = ]
{
// Called when finished!
// QNetworkRequest::FollowRedirectsAttribute would allow auto redirect
// but we use our approach as it gives us better control
const bool isRedirect = CNetworkUtils::isHttpStatusRedirect(reply);
if (isRedirect && maxRedirects > 0)
{
const QUrl redirectUrl = CNetworkUtils::getHttpRedirectUrl(reply);
if (!redirectUrl.isEmpty())
{
QNetworkRequest redirectRequest(redirectUrl);
const int redirectsLeft = maxRedirects - 1;
QTimer::singleShot(0, this, std::bind(&CApplication::httpRequestImpl, this, redirectRequest, callback, redirectsLeft, requestOrPostMethod));
return;
}
}
// called when there are no more callbacks
callback(reply);
}, Qt::QueuedConnection);
}
return reply;
}
} // ns