#include "Application.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "src/Logger/Logger.hpp" #include "src/Services/TargetService.hpp" #include "src/Services/PathService.hpp" #include "src/Services/ProcessService.hpp" #include "src/Helpers/BiMap.hpp" #include "src/Exceptions/InvalidConfig.hpp" using namespace Exceptions; Application::Application(std::vector&& arguments) : arguments(std::move(arguments)) {} int Application::run() { try { this->setName("bloom"); if (this->arguments.size() > 1) { auto& firstArg = this->arguments.at(1); const auto commandHandlersByCommandName = this->getCommandHandlersByCommandName(); const auto commandHandlerIt = commandHandlersByCommandName.find(firstArg); if (commandHandlerIt != commandHandlersByCommandName.end()) { const auto returnValue = commandHandlerIt->second(); this->shutdown(); return returnValue; } this->selectedEnvironmentName = std::move(firstArg); } if (Services::ProcessService::isRunningAsRoot()) { Logger::warning("Please don't run Bloom as root - you're asking for trouble."); } #ifdef BLOOM_DEBUG_BUILD Logger::warning("Debug build"); #endif #ifdef EXCLUDE_INSIGHT Logger::warning( "The Insight component has been excluded from this build. All Insight related configuration parameters " "will be ignored." ); #endif this->startup(); #ifndef EXCLUDE_INSIGHT if (this->insightConfig->activateOnStartup) { this->activateInsight(); } #endif this->checkBloomVersion(); /* * We use a QTimer to dispatch our events on an interval. * * This allows us to use Qt's event loop whilst still being able to process our own events. */ auto* eventDispatchTimer = new QTimer{this->qtApplication.get()}; QObject::connect(eventDispatchTimer, &QTimer::timeout, this, &Application::dispatchEvents); eventDispatchTimer->start(100); this->qtApplication->exec(); } catch (const InvalidConfig& exception) { Logger::error("Invalid project configuration (bloom.yaml) - " + exception.getMessage()); } catch (const Exception& exception) { Logger::error(exception.getMessage()); } this->shutdown(); return EXIT_SUCCESS; } std::map> Application::getCommandHandlersByCommandName() { return std::map>{ { "--help", std::bind(&Application::presentHelpText, this) }, { "-h", std::bind(&Application::presentHelpText, this) }, { "--version", std::bind(&Application::presentVersionText, this) }, { "-v", std::bind(&Application::presentVersionText, this) }, { "--version-machine", std::bind(&Application::presentVersionMachineText, this) }, { "init", std::bind(&Application::initProject, this) }, { "--capabilities-machine", std::bind(&Application::presentCapabilitiesMachine, this) }, }; } void Application::startup() { Thread::blockAllSignals(); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); #ifndef BLOOM_DEBUG_BUILD QCoreApplication::addLibraryPath(QString::fromStdString(Services::PathService::applicationDirPath() + "/plugins")), #endif #ifndef EXCLUDE_INSIGHT this->qtApplication = std::make_unique(this->qtApplicationArgc, this->qtApplicationArgv.data()); #else this->qtApplication = std::make_unique(this->qtApplicationArgc, this->qtApplicationArgv.data()); #endif auto& applicationEventListener = this->applicationEventListener; EventManager::registerListener(applicationEventListener); applicationEventListener->registerCallbackForEventType( std::bind(&Application::onShutdownApplicationRequest, this, std::placeholders::_1) ); this->loadProjectSettings(); this->loadProjectConfiguration(); Logger::configure(this->projectConfig.value()); Logger::debug("Bloom version: " + Application::VERSION.toString()); this->startSignalHandler(); Logger::info("Selected environment: \"" + this->selectedEnvironmentName + "\""); Logger::debug( "Number of environments extracted from config: " + std::to_string(this->projectConfig->environments.size()) ); applicationEventListener->registerCallbackForEventType( std::bind(&Application::onTargetControllerThreadStateChanged, this, std::placeholders::_1) ); applicationEventListener->registerCallbackForEventType( std::bind(&Application::onDebugServerThreadStateChanged, this, std::placeholders::_1) ); applicationEventListener->registerCallbackForEventType( std::bind(&Application::onDebugSessionFinished, this, std::placeholders::_1) ); #ifndef EXCLUDE_INSIGHT applicationEventListener->registerCallbackForEventType( std::bind(&Application::onInsightActivationRequest, this, std::placeholders::_1) ); applicationEventListener->registerCallbackForEventType( std::bind(&Application::onInsightMainWindowClosed, this, std::placeholders::_1) ); #endif this->startTargetController(); this->startDebugServer(); Thread::threadState = ThreadState::READY; } void Application::shutdown() { const auto appState = Thread::getThreadState(); if (appState == ThreadState::STOPPED || appState == ThreadState::SHUTDOWN_INITIATED) { return; } Thread::threadState = ThreadState::SHUTDOWN_INITIATED; Logger::info("Shutting down Bloom"); this->stopDebugServer(); this->stopTargetController(); this->stopSignalHandler(); try { this->saveProjectSettings(); } catch (const Exception& exception) { Logger::error("Failed to save project settings - " + exception.getMessage()); } Thread::threadState = ThreadState::STOPPED; } void Application::triggerShutdown() { #ifndef EXCLUDE_INSIGHT if (this->insight != nullptr) { this->insight->shutdown(); } #endif this->qtApplication->exit(EXIT_SUCCESS); } void Application::loadProjectSettings() { const auto projectSettingsPath = Services::PathService::projectSettingsPath(); auto jsonSettingsFile = QFile{QString::fromStdString(projectSettingsPath)}; if (jsonSettingsFile.exists()) { try { if (!jsonSettingsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { throw Exception{"Failed to open settings file."}; } this->projectSettings = ProjectSettings{QJsonDocument::fromJson(jsonSettingsFile.readAll()).object()}; jsonSettingsFile.close(); return; } catch (const std::exception& exception) { Logger::error( "Failed to load project settings from " + projectSettingsPath + " - " + exception.what() ); } } this->projectSettings = ProjectSettings(); } void Application::saveProjectSettings() { if (!this->projectSettings.has_value()) { return; } const auto projectSettingsPath = Services::PathService::projectSettingsPath(); auto jsonSettingsFile = QFile{QString::fromStdString(projectSettingsPath)}; Logger::debug("Saving project settings to " + projectSettingsPath); QDir{}.mkpath(QString::fromStdString(Services::PathService::projectSettingsDirPath())); try { const auto jsonDocument = QJsonDocument{this->projectSettings->toJson()}; if (!jsonSettingsFile.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { throw Exception{ "Failed to open/create settings file (" + projectSettingsPath + "). Check file permissions." }; } jsonSettingsFile.write(jsonDocument.toJson()); jsonSettingsFile.close(); } catch (const Exception& exception) { Logger::error("Failed to save project settings - " + exception.getMessage()); } } void Application::loadProjectConfiguration() { auto configFile = QFile{QString::fromStdString(Services::PathService::projectConfigPath())}; if (!configFile.exists()) { throw Exception{ "Bloom configuration file (bloom.yaml) not found. Working directory: " + Services::PathService::projectDirPath() }; } if (!configFile.open(QIODevice::ReadOnly | QIODevice::Text)) { throw InvalidConfig{ "Failed to open Bloom configuration file. Working directory: " + Services::PathService::projectDirPath() }; } try { const auto configNode = YAML::Load(configFile.readAll().toStdString()); configFile.close(); this->projectConfig = ProjectConfig(configNode); } catch (const YAML::Exception& exception) { throw InvalidConfig(exception.msg); } // Validate the selected environment const auto selectedEnvironmentIt = this->projectConfig->environments.find(this->selectedEnvironmentName); if (selectedEnvironmentIt == this->projectConfig->environments.end()) { throw InvalidConfig{ "Environment (\"" + this->selectedEnvironmentName + "\") not found in configuration." }; } this->environmentConfig = selectedEnvironmentIt->second; #ifndef EXCLUDE_INSIGHT this->insightConfig = this->environmentConfig->insightConfig.value_or(this->projectConfig->insightConfig); #endif if (this->environmentConfig->debugServerConfig.has_value()) { this->debugServerConfig = this->environmentConfig->debugServerConfig.value(); } else if (this->projectConfig->debugServerConfig.has_value()) { this->debugServerConfig = this->projectConfig->debugServerConfig.value(); } else { throw InvalidConfig{"Debug server configuration missing."}; } } int Application::presentHelpText() { /* * Silence all logging here, as we're just to display the help text and then exit the application. Any * further logging will just be noise. */ Logger::silence(); // The file help.txt is included in Bloom's binary, as a resource. See the root-level CMakeLists.txt for more. auto helpFile = QFile{ QString::fromStdString(Services::PathService::compiledResourcesPath() + "/resources/help.txt") }; if (!helpFile.open(QIODevice::ReadOnly)) { // This should never happen - if it does, something has gone very wrong throw Exception{ "Failed to open help file - please report this issue at " + Services::PathService::homeDomainName() + "/report-issue" }; } std::cout << "Bloom v" << Application::VERSION.toString() << "\n"; std::cout << QTextStream{&helpFile}.readAll().toUtf8().constData() << "\n"; return EXIT_SUCCESS; } int Application::presentVersionText() { Logger::silence(); std::cout << "Bloom v" << Application::VERSION.toString() << "\n"; #ifdef EXCLUDE_INSIGHT std::cout << "Insight has been excluded from this build.\n"; #endif #ifdef BLOOM_DEBUG_BUILD std::cout << "DEBUG BUILD - Compilation timestamp: " << __DATE__ << " " << __TIME__ << "\n"; #endif std::cout << Services::PathService::homeDomainName() + "/\n"; std::cout << "Nav Mohammed\n"; return EXIT_SUCCESS; } int Application::presentVersionMachineText() { Logger::silence(); std::cout << QJsonDocument{QJsonObject{ {"version", QString::fromStdString(Application::VERSION.toString())}, {"components", QJsonObject({ {"major", Application::VERSION.major}, {"minor", Application::VERSION.minor}, {"patch", Application::VERSION.patch}, })}, }}.toJson().toStdString(); return EXIT_SUCCESS; } int Application::presentCapabilitiesMachine() { using Targets::TargetFamily; Logger::silence(); static const auto targetFamilyNames = BiMap{ {TargetFamily::AVR_8, "AVR8"}, {TargetFamily::RISC_V, "RISC-V"}, }; auto supportedTargets = QJsonArray{}; for (const auto& [configValue, descriptor] : Services::TargetService::briefDescriptorsByConfigValue()) { supportedTargets.push_back(QJsonObject{ {"name" , QString::fromStdString(descriptor.name)}, {"family" , targetFamilyNames.at(descriptor.family)}, {"configurationValue" , QString::fromStdString(configValue)}, }); } std::cout << QJsonDocument{QJsonObject{ {"targets", supportedTargets}, #ifndef EXCLUDE_INSIGHT {"insight", true}, #else {"insight", false}, #endif }}.toJson().toStdString(); return EXIT_SUCCESS; } int Application::initProject() { auto configFile = QFile{QString::fromStdString(Services::PathService::projectConfigPath())}; if (configFile.exists()) { throw Exception{"Bloom configuration file (bloom.yaml) already exists in working directory."}; } /* * The file bloom.template.yaml is just a template Bloom config file that is included in Bloom's binary, as a * resource. See the root-level CMakeLists.txt for more. * * We simply copy the template file into the user's working directory. */ auto templateConfigFile = QFile{ QString::fromStdString(Services::PathService::compiledResourcesPath()+ "/resources/bloom.template.yaml") }; if (!templateConfigFile.open(QIODevice::ReadOnly)) { throw Exception{ "Failed to open template configuration file - please report this issue at " + Services::PathService::homeDomainName() + "/report-issue" }; } if (!configFile.open(QIODevice::ReadWrite)) { throw Exception{"Failed to create Bloom configuration file (bloom.yaml)"}; } configFile.write(templateConfigFile.readAll()); configFile.close(); templateConfigFile.close(); Logger::info("Bloom configuration file (bloom.yaml) created in working directory."); return EXIT_SUCCESS; } void Application::startSignalHandler() { this->signalHandlerThread = std::thread(&SignalHandler::run, std::ref(this->signalHandler)); } void Application::stopSignalHandler() { const auto shThreadState = this->signalHandler.getThreadState(); if (shThreadState != ThreadState::STOPPED && shThreadState != ThreadState::UNINITIALISED) { this->signalHandler.triggerShutdown(); /* * Send meaningless signal to the SignalHandler thread to have it shutdown. The signal will pull it out of * a blocking state and allow it to action the shutdown. See SignalHandler::run() for more. */ ::pthread_kill(this->signalHandlerThread.native_handle(), SIGUSR1); } if (this->signalHandlerThread.joinable()) { Logger::debug("Joining SignalHandler thread"); this->signalHandlerThread.join(); Logger::debug("SignalHandler thread joined"); } } void Application::startTargetController() { this->targetController = std::make_unique( this->projectConfig.value(), this->environmentConfig.value() ); this->targetControllerThread = std::thread( &TargetController::TargetControllerComponent::run, this->targetController.get() ); const auto tcStateChangeEvent = this->applicationEventListener->waitForEvent< Events::TargetControllerThreadStateChanged >(); if (!tcStateChangeEvent.has_value() || tcStateChangeEvent->get()->getState() != ThreadState::READY) { throw Exception{"TargetController failed to start up"}; } } void Application::stopTargetController() { if (this->targetController == nullptr) { return; } const auto tcThreadState = this->targetController->getThreadState(); if (tcThreadState == ThreadState::STARTING || tcThreadState == ThreadState::READY) { EventManager::triggerEvent(std::make_shared()); this->applicationEventListener->waitForEvent( std::chrono::milliseconds{10000} ); } if (this->targetControllerThread.joinable()) { Logger::debug("Joining TargetController thread"); this->targetControllerThread.join(); Logger::debug("TargetController thread joined"); } } void Application::startDebugServer() { this->debugServer = std::make_unique( this->debugServerConfig.value() ); this->debugServerThread = std::thread{&DebugServer::DebugServerComponent::run, this->debugServer.get()}; const auto dsStateChangeEvent = this->applicationEventListener->waitForEvent< Events::DebugServerThreadStateChanged >(); if (!dsStateChangeEvent.has_value() || dsStateChangeEvent->get()->getState() != ThreadState::READY) { throw Exception{"DebugServer failed to start up"}; } } void Application::stopDebugServer() { if (this->debugServer == nullptr) { return; } const auto debugServerState = this->debugServer->getThreadState(); if (debugServerState == ThreadState::STARTING || debugServerState == ThreadState::READY) { EventManager::triggerEvent(std::make_shared()); this->applicationEventListener->waitForEvent( std::chrono::milliseconds{5000} ); } if (this->debugServerThread.joinable()) { Logger::debug("Joining DebugServer thread"); this->debugServerThread.join(); Logger::debug("DebugServer thread joined"); } } void Application::dispatchEvents() { this->applicationEventListener->dispatchCurrentEvents(); } void Application::checkBloomVersion() { const auto currentVersionNumber = Application::VERSION; auto* networkAccessManager = new QNetworkAccessManager{this}; auto queryVersionEndpointUrl = QUrl{ QString::fromStdString(Services::PathService::homeDomainName() + "/latest-version") }; queryVersionEndpointUrl.setScheme("http"); queryVersionEndpointUrl.setQuery(QUrlQuery{ {"currentVersionNumber", QString::fromStdString(currentVersionNumber.toString())} }); QObject::connect( networkAccessManager, &QNetworkAccessManager::finished, this, [this, currentVersionNumber] (QNetworkReply* response) { const auto jsonResponseObject = QJsonDocument::fromJson(response->readAll()).object(); const auto latestVersionNumber = VersionNumber(jsonResponseObject.value("latestVersionNumber").toString()); if (latestVersionNumber > currentVersionNumber) { Logger::warning( "Bloom v" + latestVersionNumber.toString() + " is available to download - upgrade via " + Services::PathService::homeDomainName() ); } } ); networkAccessManager->get(QNetworkRequest{queryVersionEndpointUrl}); } #ifndef EXCLUDE_INSIGHT void Application::activateInsight() { assert(!this->insight); this->insight = std::make_unique( *(this->applicationEventListener), this->projectConfig.value(), this->environmentConfig.value(), this->insightConfig.value(), this->projectSettings.value().insightSettings, this->qtApplication.get() ); } void Application::onInsightActivationRequest(const Events::InsightActivationRequested&) { if (this->insight) { // Insight has already been activated this->insight->activateMainWindow(); return; } this->activateInsight(); } void Application::onInsightMainWindowClosed(const Events::InsightMainWindowClosed& event) { if (this->insightConfig->shutdownOnClose) { this->triggerShutdown(); } } #endif void Application::onShutdownApplicationRequest(const Events::ShutdownApplication&) { Logger::debug("ShutdownApplication event received."); this->triggerShutdown(); } void Application::onTargetControllerThreadStateChanged(const Events::TargetControllerThreadStateChanged& event) { if (event.getState() == ThreadState::STOPPED || event.getState() == ThreadState::SHUTDOWN_INITIATED) { // TargetController has unexpectedly shutdown. this->triggerShutdown(); } } void Application::onDebugServerThreadStateChanged(const Events::DebugServerThreadStateChanged& event) { if (event.getState() == ThreadState::STOPPED || event.getState() == ThreadState::SHUTDOWN_INITIATED) { // DebugServer has unexpectedly shutdown - it must have encountered a fatal error. this->triggerShutdown(); } } void Application::onDebugSessionFinished(const Events::DebugSessionFinished& event) { if (this->environmentConfig->shutdownPostDebugSession || Services::ProcessService::isManagedByClion()) { this->triggerShutdown(); } }