Files
BloomPatched/src/Application.cpp

644 lines
21 KiB
C++
Raw Normal View History

2021-08-22 20:46:52 +01:00
#include "Application.hpp"
2021-04-04 21:04:12 +01:00
#include <iostream>
#include <QTimer>
#include <QFile>
2021-04-04 21:04:12 +01:00
#include <QJsonDocument>
#include <unistd.h>
#include <yaml-cpp/yaml.h>
#include <yaml-cpp/exceptions.h>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QUrl>
#include <QUrlQuery>
2021-04-04 21:04:12 +01:00
#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"
2022-02-06 13:45:35 +00:00
#include "src/Exceptions/InvalidConfig.hpp"
2021-04-04 21:04:12 +01:00
using namespace Exceptions;
Application::Application(std::vector<std::string>&& arguments)
: arguments(std::move(arguments))
{}
2022-06-22 22:24:27 +01:00
int Application::run() {
try {
this->setName("bloom");
2021-04-04 21:04:12 +01:00
if (this->arguments.size() > 1) {
auto& firstArg = this->arguments.at(1);
const auto commandHandlersByCommandName = this->getCommandHandlersByCommandName();
const auto commandHandlerIt = commandHandlersByCommandName.find(firstArg);
2021-04-04 21:04:12 +01:00
if (commandHandlerIt != commandHandlersByCommandName.end()) {
const auto returnValue = commandHandlerIt->second();
2022-01-02 20:45:14 +00:00
this->shutdown();
return returnValue;
}
2021-04-04 21:04:12 +01:00
this->selectedEnvironmentName = std::move(firstArg);
}
if (Services::ProcessService::isRunningAsRoot()) {
Logger::warning("Please don't run Bloom as root - you're asking for trouble.");
}
2023-08-13 14:55:00 +01:00
2021-04-24 16:22:04 +01:00
#ifdef BLOOM_DEBUG_BUILD
Logger::warning("This is a debug build - some functions may not work as expected");
2021-04-24 16:22:04 +01:00
#endif
2023-05-10 19:53:39 +01:00
#ifdef EXCLUDE_INSIGHT
Logger::warning(
"The Insight component has been excluded from this build. All Insight related configuration parameters "
"will be ignored."
);
2023-05-10 19:53:39 +01:00
#endif
this->startup();
2021-04-04 21:04:12 +01:00
2023-05-10 19:53:39 +01:00
#ifndef EXCLUDE_INSIGHT
if (this->insightConfig->activateOnStartup) {
this->activateInsight();
}
2023-05-10 19:53:39 +01:00
#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());
2021-04-04 21:04:12 +01:00
} catch (const Exception& exception) {
Logger::error(exception.getMessage());
}
2021-04-04 21:04:12 +01:00
this->shutdown();
return EXIT_SUCCESS;
}
2021-04-04 21:04:12 +01:00
std::map<std::string, std::function<int()>> Application::getCommandHandlersByCommandName() {
return std::map<std::string, std::function<int()>>{
{
"--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<QApplication>(this->qtApplicationArgc, this->qtApplicationArgv.data());
#else
this->qtApplication = std::make_unique<QCoreApplication>(this->qtApplicationArgc, this->qtApplicationArgv.data());
#endif
auto& applicationEventListener = this->applicationEventListener;
EventManager::registerListener(applicationEventListener);
applicationEventListener->registerCallbackForEventType<Events::ShutdownApplication>(
std::bind(&Application::onShutdownApplicationRequest, this, std::placeholders::_1)
);
2021-04-04 21:04:12 +01:00
this->loadProjectSettings();
this->loadProjectConfiguration();
Logger::configure(this->projectConfig.value());
2021-04-04 21:04:12 +01:00
Logger::debug("Bloom version: " + Application::VERSION.toString());
this->startSignalHandler();
2021-04-04 21:04:12 +01:00
Logger::info("Selected environment: \"" + this->selectedEnvironmentName + "\"");
2024-07-25 19:04:13 +01:00
Logger::debug(
"Number of environments extracted from config: " + std::to_string(this->projectConfig->environments.size())
);
2021-04-04 21:04:12 +01:00
applicationEventListener->registerCallbackForEventType<Events::TargetControllerThreadStateChanged>(
std::bind(&Application::onTargetControllerThreadStateChanged, this, std::placeholders::_1)
);
applicationEventListener->registerCallbackForEventType<Events::DebugServerThreadStateChanged>(
std::bind(&Application::onDebugServerThreadStateChanged, this, std::placeholders::_1)
);
applicationEventListener->registerCallbackForEventType<Events::DebugSessionFinished>(
std::bind(&Application::onDebugSessionFinished, this, std::placeholders::_1)
);
#ifndef EXCLUDE_INSIGHT
applicationEventListener->registerCallbackForEventType<Events::InsightActivationRequested>(
std::bind(&Application::onInsightActivationRequest, this, std::placeholders::_1)
);
applicationEventListener->registerCallbackForEventType<Events::InsightMainWindowClosed>(
std::bind(&Application::onInsightMainWindowClosed, this, std::placeholders::_1)
);
#endif
this->startTargetController();
this->startDebugServer();
2021-04-04 21:04:12 +01:00
Thread::threadState = ThreadState::READY;
}
2021-04-04 21:04:12 +01:00
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");
2023-05-30 00:11:41 +01:00
this->stopDebugServer();
this->stopTargetController();
this->stopSignalHandler();
2023-05-30 00:11:41 +01:00
try {
this->saveProjectSettings();
2023-05-30 00:11:41 +01:00
} catch (const Exception& exception) {
Logger::error("Failed to save project settings - " + exception.getMessage());
2023-05-30 00:11:41 +01:00
}
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."};
}
2022-01-22 16:14:29 +00:00
this->projectSettings = ProjectSettings{QJsonDocument::fromJson(jsonSettingsFile.readAll()).object()};
jsonSettingsFile.close();
2022-01-22 16:14:29 +00:00
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)};
2022-01-22 16:14:29 +00:00
Logger::debug("Saving project settings to " + projectSettingsPath);
2022-01-22 16:14:29 +00:00
QDir{}.mkpath(QString::fromStdString(Services::PathService::projectSettingsDirPath()));
2022-01-22 16:14:29 +00:00
try {
const auto jsonDocument = QJsonDocument{this->projectSettings->toJson()};
2022-01-22 16:14:29 +00:00
if (!jsonSettingsFile.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) {
throw Exception{
"Failed to open/create settings file (" + projectSettingsPath + "). Check file permissions."
};
2022-01-22 16:14:29 +00:00
}
jsonSettingsFile.write(jsonDocument.toJson());
jsonSettingsFile.close();
} catch (const Exception& exception) {
Logger::error("Failed to save project settings - " + exception.getMessage());
2022-01-22 16:14:29 +00:00
}
}
2022-01-22 16:14:29 +00:00
void Application::loadProjectConfiguration() {
auto configFile = QFile{QString::fromStdString(Services::PathService::projectConfigPath())};
2021-04-04 21:04:12 +01:00
if (!configFile.exists()) {
throw Exception{
"Bloom configuration file (bloom.yaml) not found. Working directory: "
+ Services::PathService::projectDirPath()
};
}
2021-04-04 21:04:12 +01:00
if (!configFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
throw InvalidConfig{
"Failed to open Bloom configuration file. Working directory: " + Services::PathService::projectDirPath()
};
}
2021-04-04 21:04:12 +01:00
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;
2023-05-10 19:53:39 +01:00
#ifndef EXCLUDE_INSIGHT
this->insightConfig = this->environmentConfig->insightConfig.value_or(this->projectConfig->insightConfig);
2023-05-10 19:53:39 +01:00
#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."};
}
}
2021-04-04 21:04:12 +01:00
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"
};
}
2021-04-04 21:04:12 +01:00
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";
2021-04-24 16:22:04 +01:00
2023-05-10 19:53:39 +01:00
#ifdef EXCLUDE_INSIGHT
std::cout << "Insight has been excluded from this build.\n";
2023-05-10 19:53:39 +01:00
#endif
2021-04-24 16:22:04 +01:00
#ifdef BLOOM_DEBUG_BUILD
std::cout << "DEBUG BUILD - Compilation timestamp: " << __DATE__ << " " << __TIME__ << "\n";
2021-04-24 16:22:04 +01:00
#endif
std::cout << Services::PathService::homeDomainName() + "/\n";
std::cout << "Nav Mohammed\n";
return EXIT_SUCCESS;
}
2021-04-04 21:04:12 +01:00
int Application::presentVersionMachineText() {
Logger::silence();
2022-05-06 19:30:43 +01:00
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() {
2024-02-12 23:02:27 +00:00
using Targets::TargetFamily;
Logger::silence();
static const auto targetFamilyNames = BiMap<TargetFamily, QString>{
{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())};
2022-05-06 19:30:43 +01:00
if (configFile.exists()) {
throw Exception{"Bloom configuration file (bloom.yaml) already exists in working directory."};
2022-05-06 19:30:43 +01:00
}
/*
* 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"
};
}
2021-04-04 21:04:12 +01:00
if (!configFile.open(QIODevice::ReadWrite)) {
throw Exception{"Failed to create Bloom configuration file (bloom.yaml)"};
}
2021-04-04 21:04:12 +01:00
configFile.write(templateConfigFile.readAll());
2021-04-04 21:04:12 +01:00
configFile.close();
templateConfigFile.close();
2021-04-04 21:04:12 +01:00
Logger::info("Bloom configuration file (bloom.yaml) created in working directory.");
return EXIT_SUCCESS;
}
2021-04-04 21:04:12 +01:00
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");
}
}
2021-04-04 21:04:12 +01:00
void Application::startTargetController() {
this->targetController = std::make_unique<TargetController::TargetControllerComponent>(
this->projectConfig.value(),
this->environmentConfig.value()
);
2022-05-23 23:50:10 +01:00
this->targetControllerThread = std::thread(
&TargetController::TargetControllerComponent::run,
this->targetController.get()
);
2021-04-04 21:04:12 +01:00
const auto tcStateChangeEvent = this->applicationEventListener->waitForEvent<
Events::TargetControllerThreadStateChanged
>();
2021-04-04 21:04:12 +01:00
if (!tcStateChangeEvent.has_value() || tcStateChangeEvent->get()->getState() != ThreadState::READY) {
throw Exception{"TargetController failed to start up"};
2021-04-04 21:04:12 +01:00
}
}
2021-04-04 21:04:12 +01:00
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<Events::ShutdownTargetController>());
this->applicationEventListener->waitForEvent<Events::TargetControllerThreadStateChanged>(
std::chrono::milliseconds{10000}
2021-04-04 21:04:12 +01:00
);
}
if (this->targetControllerThread.joinable()) {
Logger::debug("Joining TargetController thread");
this->targetControllerThread.join();
Logger::debug("TargetController thread joined");
2021-04-04 21:04:12 +01:00
}
}
2021-04-04 21:04:12 +01:00
void Application::startDebugServer() {
this->debugServer = std::make_unique<DebugServer::DebugServerComponent>(
this->debugServerConfig.value()
);
this->debugServerThread = std::thread{&DebugServer::DebugServerComponent::run, this->debugServer.get()};
2021-04-04 21:04:12 +01:00
const auto dsStateChangeEvent = this->applicationEventListener->waitForEvent<
Events::DebugServerThreadStateChanged
>();
2021-04-04 21:04:12 +01:00
if (!dsStateChangeEvent.has_value() || dsStateChangeEvent->get()->getState() != ThreadState::READY) {
throw Exception{"DebugServer failed to start up"};
2021-04-04 21:04:12 +01:00
}
}
2021-04-04 21:04:12 +01:00
void Application::stopDebugServer() {
if (this->debugServer == nullptr) {
return;
2021-04-04 21:04:12 +01:00
}
const auto debugServerState = this->debugServer->getThreadState();
if (debugServerState == ThreadState::STARTING || debugServerState == ThreadState::READY) {
EventManager::triggerEvent(std::make_shared<Events::ShutdownDebugServer>());
this->applicationEventListener->waitForEvent<Events::DebugServerThreadStateChanged>(
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<Insight>(
*(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) {
2023-05-30 00:11:41 +01:00
this->triggerShutdown();
2022-02-09 17:49:25 +00:00
}
}
#endif
2022-02-09 17:49:25 +00:00
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();
2021-04-04 21:04:12 +01:00
}
}
2021-04-04 21:04:12 +01:00
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();
2021-04-04 21:04:12 +01:00
}
}
void Application::onDebugSessionFinished(const Events::DebugSessionFinished& event) {
if (this->environmentConfig->shutdownPostDebugSession || Services::ProcessService::isManagedByClion()) {
this->triggerShutdown();
}
2021-04-04 21:04:12 +01:00
}