TargetController suspension
This commit is contained in:
26
src/DebugServers/GdbRsp/Exceptions/DebugSessionAborted.hpp
Normal file
26
src/DebugServers/GdbRsp/Exceptions/DebugSessionAborted.hpp
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "src/Exceptions/Exception.hpp"
|
||||||
|
|
||||||
|
namespace Bloom::DebugServers::Gdb::Exceptions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The GDB server may abort a debug session with the client, if an internal error occurs. One circumstance where
|
||||||
|
* this can happen is when the TargetController is not able to service the debug session for whatever reason.
|
||||||
|
*
|
||||||
|
* See GdbRspDebugServer::serve() for handling code.
|
||||||
|
*/
|
||||||
|
class DebugSessionAborted: public Bloom::Exceptions::Exception
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit DebugSessionAborted(const std::string& message): Bloom::Exceptions::Exception(message) {
|
||||||
|
this->message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit DebugSessionAborted(const char* message): Bloom::Exceptions::Exception(message) {
|
||||||
|
this->message = std::string(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit DebugSessionAborted() = default;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -105,7 +105,19 @@ void GdbRspDebugServer::serve() {
|
|||||||
try {
|
try {
|
||||||
if (!this->clientConnection.has_value()) {
|
if (!this->clientConnection.has_value()) {
|
||||||
Logger::info("Waiting for GDB RSP connection");
|
Logger::info("Waiting for GDB RSP connection");
|
||||||
this->waitForConnection();
|
|
||||||
|
while (!this->clientConnection.has_value()) {
|
||||||
|
this->waitForConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Before proceeding with a new debug session, we must ensure that the TargetController is able to
|
||||||
|
* service it.
|
||||||
|
*/
|
||||||
|
if (!this->targetControllerConsole.isTargetControllerInService()) {
|
||||||
|
this->closeClientConnection();
|
||||||
|
throw DebugSessionAborted("TargetController not in service");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto packets = this->clientConnection->readPackets();
|
auto packets = this->clientConnection->readPackets();
|
||||||
@@ -121,12 +133,17 @@ void GdbRspDebugServer::serve() {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
} catch (const ClientCommunicationError& exception) {
|
} catch (const ClientCommunicationError& exception) {
|
||||||
Logger::error("GDB client communication error - " + exception.getMessage() + " - closing connection");
|
Logger::error("GDB RSP client communication error - " + exception.getMessage() + " - closing connection");
|
||||||
this->closeClientConnection();
|
this->closeClientConnection();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
} catch (const ClientNotSupported& exception) {
|
} catch (const ClientNotSupported& exception) {
|
||||||
Logger::error("Invalid GDB client - " + exception.getMessage() + " - closing connection");
|
Logger::error("Invalid GDB RSP client - " + exception.getMessage() + " - closing connection");
|
||||||
|
this->closeClientConnection();
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch (const DebugSessionAborted& exception) {
|
||||||
|
Logger::warning("GDB debug session aborted - " + exception.getMessage());
|
||||||
this->closeClientConnection();
|
this->closeClientConnection();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -163,13 +180,9 @@ void GdbRspDebugServer::waitForConnection() {
|
|||||||
|
|
||||||
this->clientConnection = Connection(this->interruptEventNotifier);
|
this->clientConnection = Connection(this->interruptEventNotifier);
|
||||||
this->clientConnection->accept(this->serverSocketFileDescriptor);
|
this->clientConnection->accept(this->serverSocketFileDescriptor);
|
||||||
|
|
||||||
Logger::info("Accepted GDP RSP connection from " + this->clientConnection->getIpAddress());
|
|
||||||
this->eventManager.triggerEvent(std::make_shared<Events::DebugSessionStarted>());
|
this->eventManager.triggerEvent(std::make_shared<Events::DebugSessionStarted>());
|
||||||
|
|
||||||
} else {
|
Logger::info("Accepted GDP RSP connection from " + this->clientConnection->getIpAddress());
|
||||||
// This method should not return until a connection has been established (or an exception is thrown)
|
|
||||||
return this->waitForConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ namespace Bloom::DebugToolDrivers
|
|||||||
|
|
||||||
void close() override;
|
void close() override;
|
||||||
|
|
||||||
|
|
||||||
Protocols::CmsisDap::Edbg::EdbgInterface& getEdbgInterface() {
|
Protocols::CmsisDap::Edbg::EdbgInterface& getEdbgInterface() {
|
||||||
return this->edbgInterface;
|
return this->edbgInterface;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ namespace Bloom::DebugToolDrivers
|
|||||||
|
|
||||||
void close() override;
|
void close() override;
|
||||||
|
|
||||||
|
|
||||||
Protocols::CmsisDap::Edbg::EdbgInterface& getEdbgInterface() {
|
Protocols::CmsisDap::Edbg::EdbgInterface& getEdbgInterface() {
|
||||||
return this->edbgInterface;
|
return this->edbgInterface;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ namespace Bloom::DebugToolDrivers
|
|||||||
|
|
||||||
void close() override;
|
void close() override;
|
||||||
|
|
||||||
|
|
||||||
Protocols::CmsisDap::Edbg::EdbgInterface& getEdbgInterface() {
|
Protocols::CmsisDap::Edbg::EdbgInterface& getEdbgInterface() {
|
||||||
return this->edbgInterface;
|
return this->edbgInterface;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ void EventListener::clearAllCallbacks() {
|
|||||||
void EventListener::registerEvent(GenericEventPointer event) {
|
void EventListener::registerEvent(GenericEventPointer event) {
|
||||||
auto eventName = event->getName();
|
auto eventName = event->getName();
|
||||||
Logger::debug("Event \"" + eventName + "\" (" + std::to_string(event->id)
|
Logger::debug("Event \"" + eventName + "\" (" + std::to_string(event->id)
|
||||||
+ ") registered for listener " + std::to_string(this->id));
|
+ ") registered for listener " + this->name);
|
||||||
auto queueLock = this->eventQueueByEventType.acquireLock();
|
auto queueLock = this->eventQueueByEventType.acquireLock();
|
||||||
auto& eventQueueByType = this->eventQueueByEventType.getReference();
|
auto& eventQueueByType = this->eventQueueByEventType.getReference();
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ std::vector<GenericEventPointer> EventListener::getEvents() {
|
|||||||
|
|
||||||
void EventListener::dispatchEvent(GenericEventPointer event) {
|
void EventListener::dispatchEvent(GenericEventPointer event) {
|
||||||
auto eventName = event->getName();
|
auto eventName = event->getName();
|
||||||
Logger::debug("Dispatching event " + eventName + ".");
|
Logger::debug("Dispatching event " + eventName + " (" + std::to_string(event->id) + ").");
|
||||||
// Dispatch the event to all registered handlers
|
// Dispatch the event to all registered handlers
|
||||||
auto mappingLock = this->eventTypeToCallbacksMapping.acquireLock();
|
auto mappingLock = this->eventTypeToCallbacksMapping.acquireLock();
|
||||||
auto& callbacks = this->eventTypeToCallbacksMapping.getReference().find(eventName)->second;
|
auto& callbacks = this->eventTypeToCallbacksMapping.getReference().find(eventName)->second;
|
||||||
|
|||||||
@@ -141,6 +141,40 @@ namespace Bloom
|
|||||||
this->registeredEventTypes.getReference().insert(EventType::name);
|
this->registeredEventTypes.getReference().insert(EventType::name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all registered callbacks for a specific event type.
|
||||||
|
*
|
||||||
|
* @tparam EventType
|
||||||
|
*/
|
||||||
|
template<class EventType>
|
||||||
|
void deregisterCallbacksForEventType() {
|
||||||
|
static_assert(
|
||||||
|
std::is_base_of<Events::Event, EventType>::value,
|
||||||
|
"EventType is not a derivation of Event"
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto mappingLock = this->eventTypeToCallbacksMapping.acquireLock();
|
||||||
|
auto& mapping = this->eventTypeToCallbacksMapping.getReference();
|
||||||
|
|
||||||
|
if (mapping.contains(EventType::name)) {
|
||||||
|
mapping.at(EventType::name).clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto registeredEventTypesLock = this->registeredEventTypes.acquireLock();
|
||||||
|
this->registeredEventTypes.getReference().erase(EventType::name);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto queueLock = this->eventQueueByEventType.acquireLock();
|
||||||
|
auto& eventQueueByType = this->eventQueueByEventType.getReference();
|
||||||
|
|
||||||
|
if (eventQueueByType.contains(EventType::name)) {
|
||||||
|
eventQueueByType.erase(EventType::name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for an event (of type EventTypeA, EventTypeB or EventTypeC) to be dispatched to the listener.
|
* Waits for an event (of type EventTypeA, EventTypeB or EventTypeC) to be dispatched to the listener.
|
||||||
* Then returns the event object. If timeout is reached, an std::nullopt object will be returned.
|
* Then returns the event object. If timeout is reached, an std::nullopt object will be returned.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "DebugSessionStarted.hpp"
|
#include "DebugSessionStarted.hpp"
|
||||||
#include "DebugSessionFinished.hpp"
|
#include "DebugSessionFinished.hpp"
|
||||||
#include "TargetControllerThreadStateChanged.hpp"
|
#include "TargetControllerThreadStateChanged.hpp"
|
||||||
|
#include "ReportTargetControllerState.hpp"
|
||||||
|
#include "TargetControllerStateReported.hpp"
|
||||||
#include "ShutdownTargetController.hpp"
|
#include "ShutdownTargetController.hpp"
|
||||||
#include "TargetControllerErrorOccurred.hpp"
|
#include "TargetControllerErrorOccurred.hpp"
|
||||||
#include "ShutdownApplication.hpp"
|
#include "ShutdownApplication.hpp"
|
||||||
@@ -33,7 +35,7 @@
|
|||||||
#include "ProgramCounterSetOnTarget.hpp"
|
#include "ProgramCounterSetOnTarget.hpp"
|
||||||
#include "ExtractTargetDescriptor.hpp"
|
#include "ExtractTargetDescriptor.hpp"
|
||||||
#include "TargetDescriptorExtracted.hpp"
|
#include "TargetDescriptorExtracted.hpp"
|
||||||
#include "InsightStateChanged.hpp"
|
#include "InsightThreadStateChanged.hpp"
|
||||||
#include "RetrieveTargetPinStates.hpp"
|
#include "RetrieveTargetPinStates.hpp"
|
||||||
#include "TargetPinStatesRetrieved.hpp"
|
#include "TargetPinStatesRetrieved.hpp"
|
||||||
#include "SetTargetPinState.hpp"
|
#include "SetTargetPinState.hpp"
|
||||||
|
|||||||
@@ -7,19 +7,17 @@
|
|||||||
|
|
||||||
namespace Bloom::Events
|
namespace Bloom::Events
|
||||||
{
|
{
|
||||||
class InsightStateChanged: public Event
|
class InsightThreadStateChanged: public Event
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
ThreadState state;
|
ThreadState state;
|
||||||
public:
|
public:
|
||||||
InsightStateChanged(ThreadState state): state(state) {
|
InsightThreadStateChanged(ThreadState state): state(state) {};
|
||||||
|
|
||||||
};
|
static inline const std::string name = "InsightThreadStateChanged";
|
||||||
|
|
||||||
static inline const std::string name = "InsightStateChanged";
|
|
||||||
|
|
||||||
std::string getName() const override {
|
std::string getName() const override {
|
||||||
return InsightStateChanged::name;
|
return InsightThreadStateChanged::name;
|
||||||
}
|
}
|
||||||
|
|
||||||
ThreadState getState() const {
|
ThreadState getState() const {
|
||||||
20
src/EventManager/Events/ReportTargetControllerState.hpp
Normal file
20
src/EventManager/Events/ReportTargetControllerState.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Event.hpp"
|
||||||
|
|
||||||
|
namespace Bloom::Events
|
||||||
|
{
|
||||||
|
class ReportTargetControllerState: public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ReportTargetControllerState() {};
|
||||||
|
|
||||||
|
static inline const std::string name = "ReportTargetControllerState";
|
||||||
|
|
||||||
|
std::string getName() const override {
|
||||||
|
return ReportTargetControllerState::name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
23
src/EventManager/Events/TargetControllerStateReported.hpp
Normal file
23
src/EventManager/Events/TargetControllerStateReported.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Event.hpp"
|
||||||
|
#include "src/TargetController/TargetControllerState.hpp"
|
||||||
|
|
||||||
|
namespace Bloom::Events
|
||||||
|
{
|
||||||
|
class TargetControllerStateReported: public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TargetControllerState state;
|
||||||
|
|
||||||
|
TargetControllerStateReported(TargetControllerState state): state(state) {};
|
||||||
|
|
||||||
|
static inline const std::string name = "TargetControllerStateReported";
|
||||||
|
|
||||||
|
std::string getName() const override {
|
||||||
|
return TargetControllerStateReported::name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "TargetController.hpp"
|
#include "TargetController.hpp"
|
||||||
#include "src/Exceptions/InvalidConfig.hpp"
|
#include "src/Exceptions/InvalidConfig.hpp"
|
||||||
#include "src/Exceptions/TargetControllerStartupFailure.hpp"
|
#include "src/Exceptions/TargetControllerStartupFailure.hpp"
|
||||||
|
#include "src/Exceptions/DeviceCommunicationFailure.hpp"
|
||||||
#include "src/Application.hpp"
|
#include "src/Application.hpp"
|
||||||
|
|
||||||
using namespace Bloom;
|
using namespace Bloom;
|
||||||
@@ -19,9 +20,27 @@ void TargetController::run() {
|
|||||||
this->setThreadStateAndEmitEvent(ThreadState::READY);
|
this->setThreadStateAndEmitEvent(ThreadState::READY);
|
||||||
Logger::debug("TargetController ready and waiting for events.");
|
Logger::debug("TargetController ready and waiting for events.");
|
||||||
|
|
||||||
while (this->getState() == ThreadState::READY) {
|
while (this->getThreadState() == ThreadState::READY) {
|
||||||
this->fireTargetEvents();
|
try {
|
||||||
this->eventListener->waitAndDispatch(60);
|
if (this->state == TargetControllerState::ACTIVE) {
|
||||||
|
this->fireTargetEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->eventListener->waitAndDispatch(60);
|
||||||
|
|
||||||
|
} catch (const DeviceCommunicationFailure& exception) {
|
||||||
|
/*
|
||||||
|
* Upon a device communication failure, we assume Bloom has lost control of the debug tool. This could
|
||||||
|
* be the result of the user disconnecting the debug tool, or issuing a soft reset.
|
||||||
|
* The soft reset could have been issued via another application, without the user's knowledge.
|
||||||
|
* See https://github.com/navnavnav/Bloom/issues/3 for more on that.
|
||||||
|
*
|
||||||
|
* The TC will go into a suspended state and the DebugServer should terminate any active debug
|
||||||
|
* session. When the user attempts to start another debug session, we will try to re-connect to the
|
||||||
|
* debug tool.
|
||||||
|
*/
|
||||||
|
this->suspend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (const TargetControllerStartupFailure& exception) {
|
} catch (const TargetControllerStartupFailure& exception) {
|
||||||
@@ -50,70 +69,11 @@ void TargetController::startup() {
|
|||||||
// Install Bloom's udev rules if not already installed
|
// Install Bloom's udev rules if not already installed
|
||||||
this->checkUdevRules();
|
this->checkUdevRules();
|
||||||
|
|
||||||
auto debugToolName = this->environmentConfig.debugToolName;
|
|
||||||
auto targetName = this->environmentConfig.targetConfig.name;
|
|
||||||
auto supportedDebugTools = TargetController::getSupportedDebugTools();
|
|
||||||
auto supportedTargets = TargetController::getSupportedTargets();
|
|
||||||
|
|
||||||
if (!supportedDebugTools.contains(debugToolName)) {
|
|
||||||
throw Exceptions::InvalidConfig(
|
|
||||||
"Debug tool name (\"" + debugToolName + "\") not recognised. Please check your configuration!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedTargets.contains(targetName)) {
|
|
||||||
throw Exceptions::InvalidConfig(
|
|
||||||
"Target name (\"" + targetName + "\") not recognised. Please check your configuration!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initiate debug tool and target
|
|
||||||
this->setDebugTool(std::move(supportedDebugTools.at(debugToolName)()));
|
|
||||||
this->setTarget(supportedTargets.at(targetName)());
|
|
||||||
|
|
||||||
Logger::info("Connecting to debug tool");
|
|
||||||
this->debugTool->init();
|
|
||||||
|
|
||||||
Logger::info("Debug tool connected");
|
|
||||||
Logger::info("Debug tool name: " + this->debugTool->getName());
|
|
||||||
Logger::info("Debug tool serial: " + this->debugTool->getSerialNumber());
|
|
||||||
|
|
||||||
if (!this->target->isDebugToolSupported(this->debugTool.get())) {
|
|
||||||
throw Exceptions::InvalidConfig(
|
|
||||||
"Debug tool (\"" + this->debugTool->getName() + "\") not supported " +
|
|
||||||
"by target (\"" + this->target->getName() + "\")."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->target->setDebugTool(this->debugTool.get());
|
|
||||||
this->target->preActivationConfigure(this->environmentConfig.targetConfig);
|
|
||||||
|
|
||||||
Logger::info("Activating target");
|
|
||||||
this->target->activate();
|
|
||||||
Logger::info("Target activated");
|
|
||||||
this->target->postActivationConfigure();
|
|
||||||
|
|
||||||
while (this->target->supportsPromotion()) {
|
|
||||||
auto promotedTarget = this->target->promote();
|
|
||||||
|
|
||||||
if (promotedTarget == nullptr
|
|
||||||
|| std::type_index(typeid(*promotedTarget)) == std::type_index(typeid(*this->target))
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->target.reset(promotedTarget.release());
|
|
||||||
this->target->postPromotionConfigure();
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger::info("Target ID: " + this->target->getHumanReadableId());
|
|
||||||
Logger::info("Target name: " + this->target->getName());
|
|
||||||
|
|
||||||
if (this->target->getState() == TargetState::STOPPED) {
|
|
||||||
this->target->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register event handlers
|
// Register event handlers
|
||||||
|
this->eventListener->registerCallbackForEventType<Events::ReportTargetControllerState>(
|
||||||
|
std::bind(&TargetController::onStateReportRequest, this, std::placeholders::_1)
|
||||||
|
);
|
||||||
|
|
||||||
this->eventListener->registerCallbackForEventType<Events::ShutdownTargetController>(
|
this->eventListener->registerCallbackForEventType<Events::ShutdownTargetController>(
|
||||||
std::bind(&TargetController::onShutdownTargetControllerEvent, this, std::placeholders::_1)
|
std::bind(&TargetController::onShutdownTargetControllerEvent, this, std::placeholders::_1)
|
||||||
);
|
);
|
||||||
@@ -122,6 +82,100 @@ void TargetController::startup() {
|
|||||||
std::bind(&TargetController::onDebugSessionStartedEvent, this, std::placeholders::_1)
|
std::bind(&TargetController::onDebugSessionStartedEvent, this, std::placeholders::_1)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this->resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::checkUdevRules() {
|
||||||
|
auto bloomRulesPath = std::string("/etc/udev/rules.d/99-bloom.rules");
|
||||||
|
auto latestBloomRulesPath = Application::getResourcesDirPath() + "/UDevRules/99-bloom.rules";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(bloomRulesPath)) {
|
||||||
|
Logger::warning("Bloom udev rules missing - attempting installation");
|
||||||
|
|
||||||
|
// We can only install them if we're running as root
|
||||||
|
if (!Application::isRunningAsRoot()) {
|
||||||
|
Logger::error("Bloom udev rules missing - cannot install udev rules without root privileges.\n"
|
||||||
|
"Running Bloom once with root privileges will allow it to automatically install the udev rules."
|
||||||
|
" Alternatively, instructions on manually installing the udev rules can be found "
|
||||||
|
"here: https://bloom.oscillate.io/docs/getting-started\nBloom may fail to connect to some debug tools "
|
||||||
|
"until this is resolved.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(latestBloomRulesPath)) {
|
||||||
|
// This shouldn't happen, but it can if someone has been messing with the installation files
|
||||||
|
Logger::error("Unable to install Bloom udev rules - \"" + latestBloomRulesPath + "\" does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::copy(latestBloomRulesPath, bloomRulesPath);
|
||||||
|
Logger::warning("Bloom udev rules installed - a reconnect of the debug tool may be required "
|
||||||
|
"before the new udev rules come into effect.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::shutdown() {
|
||||||
|
if (this->getThreadState() == ThreadState::STOPPED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger::info("Shutting down TargetController");
|
||||||
|
this->eventManager.deregisterListener(this->eventListener->getId());
|
||||||
|
this->releaseResources();
|
||||||
|
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
Logger::error("Failed to properly shutdown TargetController. Error: " + std::string(exception.what()));
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setThreadStateAndEmitEvent(ThreadState::STOPPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::suspend() {
|
||||||
|
if (this->getThreadState() != ThreadState::READY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::debug("Suspending TargetController");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this->releaseResources();
|
||||||
|
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
Logger::error("Failed to release connected debug tool and target resources. Error: "
|
||||||
|
+ std::string(exception.what()));
|
||||||
|
}
|
||||||
|
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::DebugSessionFinished>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::ExtractTargetDescriptor>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::StopTargetExecution>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::StepTargetExecution>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::ResumeTargetExecution>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::RetrieveRegistersFromTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::WriteRegistersToTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::RetrieveMemoryFromTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::WriteMemoryToTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::SetBreakpointOnTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::RemoveBreakpointOnTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::SetProgramCounterOnTarget>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::InsightThreadStateChanged>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::RetrieveTargetPinStates>();
|
||||||
|
this->eventListener->deregisterCallbacksForEventType<Events::SetTargetPinState>();
|
||||||
|
|
||||||
|
this->lastTargetState = TargetState::UNKNOWN;
|
||||||
|
this->cachedTargetDescriptor = std::nullopt;
|
||||||
|
|
||||||
|
this->state = TargetControllerState::SUSPENDED;
|
||||||
|
this->eventManager.triggerEvent(
|
||||||
|
std::make_shared<TargetControllerStateReported>(this->state)
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger::debug("TargetController suspended");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::resume() {
|
||||||
|
this->acquireResources();
|
||||||
|
|
||||||
this->eventListener->registerCallbackForEventType<Events::DebugSessionFinished>(
|
this->eventListener->registerCallbackForEventType<Events::DebugSessionFinished>(
|
||||||
std::bind(&TargetController::onDebugSessionFinishedEvent, this, std::placeholders::_1)
|
std::bind(&TargetController::onDebugSessionFinishedEvent, this, std::placeholders::_1)
|
||||||
);
|
);
|
||||||
@@ -170,7 +224,7 @@ void TargetController::startup() {
|
|||||||
std::bind(&TargetController::onSetProgramCounterEvent, this, std::placeholders::_1)
|
std::bind(&TargetController::onSetProgramCounterEvent, this, std::placeholders::_1)
|
||||||
);
|
);
|
||||||
|
|
||||||
this->eventListener->registerCallbackForEventType<Events::InsightStateChanged>(
|
this->eventListener->registerCallbackForEventType<Events::InsightThreadStateChanged>(
|
||||||
std::bind(&TargetController::onInsightStateChangedEvent, this, std::placeholders::_1)
|
std::bind(&TargetController::onInsightStateChangedEvent, this, std::placeholders::_1)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -181,68 +235,101 @@ void TargetController::startup() {
|
|||||||
this->eventListener->registerCallbackForEventType<Events::SetTargetPinState>(
|
this->eventListener->registerCallbackForEventType<Events::SetTargetPinState>(
|
||||||
std::bind(&TargetController::onSetPinStateEvent, this, std::placeholders::_1)
|
std::bind(&TargetController::onSetPinStateEvent, this, std::placeholders::_1)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
void TargetController::checkUdevRules() {
|
Logger::debug("TC resumed");
|
||||||
auto bloomRulesPath = std::string("/etc/udev/rules.d/99-bloom.rules");
|
this->state = TargetControllerState::ACTIVE;
|
||||||
auto latestBloomRulesPath = Application::getResourcesDirPath() + "/UDevRules/99-bloom.rules";
|
this->eventManager.triggerEvent(
|
||||||
|
std::make_shared<TargetControllerStateReported>(this->state)
|
||||||
|
);
|
||||||
|
|
||||||
if (!std::filesystem::exists(bloomRulesPath)) {
|
if (this->target->getState() != TargetState::RUNNING) {
|
||||||
Logger::warning("Bloom udev rules missing - attempting installation");
|
this->target->run();
|
||||||
|
|
||||||
// We can only install them if we're running as root
|
|
||||||
if (!Application::isRunningAsRoot()) {
|
|
||||||
Logger::error("Bloom udev rules missing - cannot install udev rules without root privileges.\n"
|
|
||||||
"Running Bloom once with root privileges will allow it to automatically install the udev rules."
|
|
||||||
" Alternatively, instructions on manually installing the udev rules can be found "
|
|
||||||
"here: https://bloom.oscillate.io/docs/getting-started\nBloom may fail to connect to some debug tools "
|
|
||||||
"until this is resolved.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!std::filesystem::exists(latestBloomRulesPath)) {
|
|
||||||
// This shouldn't happen, but it can if someone has been messing with the installation files
|
|
||||||
Logger::error("Unable to install Bloom udev rules - \"" + latestBloomRulesPath + "\" does not exist.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::copy(latestBloomRulesPath, bloomRulesPath);
|
|
||||||
Logger::warning("Bloom udev rules installed - a reconnect of the debug tool may be required "
|
|
||||||
"before the new udev rules come into effect.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TargetController::shutdown() {
|
void TargetController::acquireResources() {
|
||||||
if (this->getState() == ThreadState::STOPPED) {
|
auto debugToolName = this->environmentConfig.debugToolConfig.name;
|
||||||
return;
|
auto targetName = this->environmentConfig.targetConfig.name;
|
||||||
|
|
||||||
|
auto supportedDebugTools = TargetController::getSupportedDebugTools();
|
||||||
|
auto supportedTargets = TargetController::getSupportedTargets();
|
||||||
|
|
||||||
|
if (!supportedDebugTools.contains(debugToolName)) {
|
||||||
|
throw Exceptions::InvalidConfig(
|
||||||
|
"Debug tool name (\"" + debugToolName + "\") not recognised. Please check your configuration!"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!supportedTargets.contains(targetName)) {
|
||||||
Logger::info("Shutting down TargetController");
|
throw Exceptions::InvalidConfig(
|
||||||
//this->targetControllerEventListener->clearAllCallbacks();
|
"Target name (\"" + targetName + "\") not recognised. Please check your configuration!"
|
||||||
this->eventManager.deregisterListener(this->eventListener->getId());
|
);
|
||||||
auto target = this->getTarget();
|
}
|
||||||
auto debugTool = this->getDebugTool();
|
|
||||||
|
|
||||||
if (debugTool != nullptr && debugTool->isInitialised()) {
|
// Initiate debug tool and target
|
||||||
if (target != nullptr) {
|
this->debugTool = supportedDebugTools.at(debugToolName)();
|
||||||
/*
|
|
||||||
* We call deactivate() without checking if the target is activated. This will address any cases
|
|
||||||
* where a target is only partially activated (where the call to activate() failed).
|
|
||||||
*/
|
|
||||||
Logger::info("Deactivating target");
|
|
||||||
target->deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger::info("Closing debug tool");
|
Logger::info("Connecting to debug tool");
|
||||||
debugTool->close();
|
this->debugTool->init();
|
||||||
|
|
||||||
|
Logger::info("Debug tool connected");
|
||||||
|
Logger::info("Debug tool name: " + this->debugTool->getName());
|
||||||
|
Logger::info("Debug tool serial: " + this->debugTool->getSerialNumber());
|
||||||
|
|
||||||
|
this->target = supportedTargets.at(targetName)();
|
||||||
|
|
||||||
|
if (!this->target->isDebugToolSupported(this->debugTool.get())) {
|
||||||
|
throw Exceptions::InvalidConfig(
|
||||||
|
"Debug tool (\"" + this->debugTool->getName() + "\") not supported " +
|
||||||
|
"by target (\"" + this->target->getName() + "\")."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->target->setDebugTool(this->debugTool.get());
|
||||||
|
this->target->preActivationConfigure(this->environmentConfig.targetConfig);
|
||||||
|
|
||||||
|
Logger::info("Activating target");
|
||||||
|
this->target->activate();
|
||||||
|
Logger::info("Target activated");
|
||||||
|
this->target->postActivationConfigure();
|
||||||
|
|
||||||
|
while (this->target->supportsPromotion()) {
|
||||||
|
auto promotedTarget = this->target->promote();
|
||||||
|
|
||||||
|
if (promotedTarget == nullptr
|
||||||
|
|| std::type_index(typeid(*promotedTarget)) == std::type_index(typeid(*this->target))
|
||||||
|
) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (const std::exception& exception) {
|
this->target.reset(promotedTarget.release());
|
||||||
Logger::error("Failed to properly shutdown TargetController. Error: " + std::string(exception.what()));
|
this->target->postPromotionConfigure();
|
||||||
}
|
}
|
||||||
|
|
||||||
this->setStateAndEmitEvent(ThreadState::STOPPED);
|
Logger::info("Target ID: " + this->target->getHumanReadableId());
|
||||||
|
Logger::info("Target name: " + this->target->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::releaseResources() {
|
||||||
|
auto target = this->getTarget();
|
||||||
|
auto debugTool = this->getDebugTool();
|
||||||
|
|
||||||
|
if (debugTool != nullptr && debugTool->isInitialised()) {
|
||||||
|
if (target != nullptr) {
|
||||||
|
/*
|
||||||
|
* We call deactivate() without checking if the target is activated. This will address any cases
|
||||||
|
* where a target is only partially activated (where the call to activate() failed).
|
||||||
|
*/
|
||||||
|
Logger::info("Deactivating target");
|
||||||
|
target->deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info("Closing debug tool");
|
||||||
|
debugTool->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->debugTool.reset();
|
||||||
|
this->target.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TargetController::fireTargetEvents() {
|
void TargetController::fireTargetEvents() {
|
||||||
@@ -276,6 +363,38 @@ void TargetController::onShutdownTargetControllerEvent(EventPointer<Events::Shut
|
|||||||
this->shutdown();
|
this->shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TargetController::onStateReportRequest(EventPointer<Events::ReportTargetControllerState> event) {
|
||||||
|
auto stateEvent = std::make_shared<TargetControllerStateReported>(this->state);
|
||||||
|
stateEvent->correlationId = event->id;
|
||||||
|
this->eventManager.triggerEvent(stateEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::onDebugSessionStartedEvent(EventPointer<Events::DebugSessionStarted>) {
|
||||||
|
if (this->state == TargetControllerState::SUSPENDED) {
|
||||||
|
Logger::debug("Waking TargetController");
|
||||||
|
|
||||||
|
this->resume();
|
||||||
|
this->fireTargetEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->target->reset();
|
||||||
|
|
||||||
|
if (this->target->getState() != TargetState::STOPPED) {
|
||||||
|
this->target->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TargetController::onDebugSessionFinishedEvent(EventPointer<DebugSessionFinished>) {
|
||||||
|
if (this->target->getState() != TargetState::RUNNING) {
|
||||||
|
this->target->run();
|
||||||
|
this->fireTargetEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->environmentConfig.debugToolConfig.releasePostDebugSession) {
|
||||||
|
this->suspend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TargetController::onExtractTargetDescriptor(EventPointer<Events::ExtractTargetDescriptor> event) {
|
void TargetController::onExtractTargetDescriptor(EventPointer<Events::ExtractTargetDescriptor> event) {
|
||||||
if (!this->cachedTargetDescriptor.has_value()) {
|
if (!this->cachedTargetDescriptor.has_value()) {
|
||||||
this->cachedTargetDescriptor = this->target->getDescriptor();
|
this->cachedTargetDescriptor = this->target->getDescriptor();
|
||||||
@@ -444,20 +563,6 @@ void TargetController::onRemoveBreakpointEvent(EventPointer<Events::RemoveBreakp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TargetController::onDebugSessionStartedEvent(EventPointer<Events::DebugSessionStarted>) {
|
|
||||||
this->target->reset();
|
|
||||||
|
|
||||||
if (this->target->getState() != TargetState::STOPPED) {
|
|
||||||
this->target->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TargetController::onDebugSessionFinishedEvent(EventPointer<DebugSessionFinished>) {
|
|
||||||
if (this->target->getState() != TargetState::RUNNING) {
|
|
||||||
this->target->run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TargetController::onSetProgramCounterEvent(EventPointer<Events::SetProgramCounterOnTarget> event) {
|
void TargetController::onSetProgramCounterEvent(EventPointer<Events::SetProgramCounterOnTarget> event) {
|
||||||
try {
|
try {
|
||||||
if (this->target->getState() != TargetState::STOPPED) {
|
if (this->target->getState() != TargetState::STOPPED) {
|
||||||
|
|||||||
@@ -8,14 +8,18 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
|
||||||
#include "src/Helpers/Thread.hpp"
|
#include "src/Helpers/Thread.hpp"
|
||||||
#include "src/Logger/Logger.hpp"
|
#include "TargetControllerState.hpp"
|
||||||
#include "src/EventManager/EventListener.hpp"
|
|
||||||
#include "src/DebugToolDrivers/DebugTools.hpp"
|
#include "src/DebugToolDrivers/DebugTools.hpp"
|
||||||
#include "src/Targets/Target.hpp"
|
#include "src/Targets/Target.hpp"
|
||||||
#include "src/Targets/Targets.hpp"
|
#include "src/Targets/Targets.hpp"
|
||||||
|
|
||||||
#include "src/EventManager/EventManager.hpp"
|
#include "src/EventManager/EventManager.hpp"
|
||||||
|
#include "src/EventManager/EventListener.hpp"
|
||||||
#include "src/EventManager/Events/Events.hpp"
|
#include "src/EventManager/Events/Events.hpp"
|
||||||
|
|
||||||
|
#include "src/Logger/Logger.hpp"
|
||||||
|
|
||||||
namespace Bloom
|
namespace Bloom
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +34,11 @@ namespace Bloom
|
|||||||
class TargetController: public Thread
|
class TargetController: public Thread
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* The TC starts off in a suspended state. TargetController::resume() is invoked from the startup routine.
|
||||||
|
*/
|
||||||
|
TargetControllerState state = TargetControllerState::SUSPENDED;
|
||||||
|
|
||||||
ApplicationConfig applicationConfig;
|
ApplicationConfig applicationConfig;
|
||||||
EnvironmentConfig environmentConfig;
|
EnvironmentConfig environmentConfig;
|
||||||
|
|
||||||
@@ -138,14 +147,6 @@ namespace Bloom
|
|||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setDebugTool(std::unique_ptr<DebugTool> debugTool) {
|
|
||||||
this->debugTool = std::move(debugTool);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setTarget(std::unique_ptr<Targets::Target> target) {
|
|
||||||
this->target = std::move(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
Targets::Target* getTarget() {
|
Targets::Target* getTarget() {
|
||||||
return this->target.get();
|
return this->target.get();
|
||||||
}
|
}
|
||||||
@@ -186,6 +187,15 @@ namespace Bloom
|
|||||||
*/
|
*/
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
|
|
||||||
|
void suspend();
|
||||||
|
|
||||||
|
void resume();
|
||||||
|
|
||||||
|
void acquireResources();
|
||||||
|
|
||||||
|
void releaseResources();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should fire any events queued on the target.
|
* Should fire any events queued on the target.
|
||||||
*/
|
*/
|
||||||
@@ -215,6 +225,13 @@ namespace Bloom
|
|||||||
*/
|
*/
|
||||||
void run();
|
void run();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports the current state of the TargetController.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
void onStateReportRequest(Events::EventPointer<Events::ReportTargetControllerState> event);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains a TargetDescriptor from the target and includes it in a TargetDescriptorExtracted event.
|
* Obtains a TargetDescriptor from the target and includes it in a TargetDescriptorExtracted event.
|
||||||
*
|
*
|
||||||
@@ -322,7 +339,7 @@ namespace Bloom
|
|||||||
*
|
*
|
||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
void onInsightStateChangedEvent(Events::EventPointer<Events::InsightStateChanged> event);
|
void onInsightStateChangedEvent(Events::EventPointer<Events::InsightThreadStateChanged> event);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will attempt to obtain the pin states from the target. Will emit a TargetPinStatesRetrieved event on success.
|
* Will attempt to obtain the pin states from the target. Will emit a TargetPinStatesRetrieved event on success.
|
||||||
|
|||||||
@@ -9,6 +9,33 @@ using namespace Bloom::Targets;
|
|||||||
using namespace Bloom::Events;
|
using namespace Bloom::Events;
|
||||||
using namespace Bloom::Exceptions;
|
using namespace Bloom::Exceptions;
|
||||||
|
|
||||||
|
TargetControllerState TargetControllerConsole::getTargetControllerState() {
|
||||||
|
auto getStateEvent = std::make_shared<Events::ReportTargetControllerState>();
|
||||||
|
this->eventManager.triggerEvent(getStateEvent);
|
||||||
|
auto responseEvent = this->eventListener.waitForEvent<
|
||||||
|
Events::TargetControllerStateReported,
|
||||||
|
Events::TargetControllerErrorOccurred
|
||||||
|
>(this->defaultTimeout, getStateEvent->id);
|
||||||
|
|
||||||
|
if (!responseEvent.has_value()
|
||||||
|
|| !std::holds_alternative<EventPointer<Events::TargetControllerStateReported>>(responseEvent.value())
|
||||||
|
) {
|
||||||
|
throw Exception("Unexpected response from TargetController");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stateReportedEvent = std::get<EventPointer<Events::TargetControllerStateReported>>(responseEvent.value());
|
||||||
|
return stateReportedEvent->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TargetControllerConsole::isTargetControllerInService() noexcept {
|
||||||
|
try {
|
||||||
|
return this->getTargetControllerState() == TargetControllerState::ACTIVE;
|
||||||
|
|
||||||
|
} catch (const std::runtime_error&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Targets::TargetDescriptor TargetControllerConsole::getTargetDescriptor() {
|
Targets::TargetDescriptor TargetControllerConsole::getTargetDescriptor() {
|
||||||
auto extractEvent = std::make_shared<Events::ExtractTargetDescriptor>();
|
auto extractEvent = std::make_shared<Events::ExtractTargetDescriptor>();
|
||||||
this->eventManager.triggerEvent(extractEvent);
|
this->eventManager.triggerEvent(extractEvent);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include "TargetControllerState.hpp"
|
||||||
|
|
||||||
#include "src/EventManager/EventListener.hpp"
|
#include "src/EventManager/EventListener.hpp"
|
||||||
#include "src/EventManager/EventManager.hpp"
|
#include "src/EventManager/EventManager.hpp"
|
||||||
|
|
||||||
@@ -35,6 +37,10 @@ namespace Bloom
|
|||||||
this->defaultTimeout = timeout;
|
this->defaultTimeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TargetControllerState getTargetControllerState();
|
||||||
|
|
||||||
|
bool isTargetControllerInService() noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the TargetDescriptor from the TargetController
|
* Requests the TargetDescriptor from the TargetController
|
||||||
*
|
*
|
||||||
|
|||||||
10
src/TargetController/TargetControllerState.hpp
Normal file
10
src/TargetController/TargetControllerState.hpp
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Bloom
|
||||||
|
{
|
||||||
|
enum class TargetControllerState: int
|
||||||
|
{
|
||||||
|
ACTIVE,
|
||||||
|
SUSPENDED,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,3 +17,4 @@ namespace Bloom::Targets
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(Bloom::Targets::TargetDescriptor)
|
||||||
|
|||||||
Reference in New Issue
Block a user