interface added and web ui created
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "BrokerI.h"
|
||||
#include "DataSourceI.h"
|
||||
#include "DebugService.h"
|
||||
#include "DebugServiceI.h"
|
||||
#include "FastPollingMutexSem.h"
|
||||
#include "HighResolutionTimer.h"
|
||||
#include "MemoryMapBroker.h"
|
||||
@@ -90,8 +90,11 @@ public:
|
||||
// Spins while paused, then consumes one step counter tick (if stepping).
|
||||
// Input brokers must NOT call this — they complete normally to avoid blocking
|
||||
// cross-thread EventSem posts (RealTimeThreadSynchBroker would time out).
|
||||
static void OutputPauseAndStep(DebugService *service, const char8 *gamName) {
|
||||
if (service == NULL_PTR(DebugService *)) return;
|
||||
static void OutputPauseAndStep(DebugServiceI *service, const char8 *gamName) {
|
||||
if (service == NULL_PTR(DebugServiceI *)) return;
|
||||
// Fast path: nothing to do when neither paused nor stepping.
|
||||
// Avoids Threads::Name() lookup (and its mutex) on every 1000 Hz cycle.
|
||||
if (!service->IsPaused() && !service->IsStepPending()) return;
|
||||
// Wait if already paused (manual PAUSE or breakpoint from a previous cycle)
|
||||
while (service->IsPaused()) Sleep::MSec(10);
|
||||
// Pass the OS thread name so per-thread step filtering works.
|
||||
@@ -100,13 +103,13 @@ public:
|
||||
while (service->IsPaused()) Sleep::MSec(10);
|
||||
}
|
||||
|
||||
static void Process(DebugService *service,
|
||||
static void Process(DebugServiceI *service,
|
||||
DebugSignalInfo **signalInfoPointers,
|
||||
Vec<uint32> &activeIndices, Vec<uint32> &activeSizes,
|
||||
FastPollingMutexSem &activeMutex,
|
||||
volatile bool *anyBreakFlag,
|
||||
Vec<uint32> *breakIndices) {
|
||||
if (service == NULL_PTR(DebugService *))
|
||||
if (service == NULL_PTR(DebugServiceI *))
|
||||
return;
|
||||
|
||||
// NOTE: No spin here. Spinning for paused state is handled in Execute() of
|
||||
@@ -118,7 +121,7 @@ public:
|
||||
if (n > 0 && signalInfoPointers != NULL_PTR(DebugSignalInfo **)) {
|
||||
// Capture timestamp ONCE per broker cycle for lowest impact
|
||||
uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() *
|
||||
HighResolutionTimer::Period() * 1000000.0);
|
||||
HighResolutionTimer::Period() * 1.0e9);
|
||||
|
||||
for (uint32 i = 0; i < n; i++) {
|
||||
uint32 idx = activeIndices[i];
|
||||
@@ -130,13 +133,33 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional break check — zero cost when anyBreakFlag is false
|
||||
// (single volatile read; the RT loop never pays for this when no break is set).
|
||||
if (*anyBreakFlag && !service->IsPaused() &&
|
||||
breakIndices != NULL_PTR(Vec<uint32> *)) {
|
||||
uint32 nb = breakIndices->Size();
|
||||
// FIX #3: copy break indices under lock into a local stack array, then
|
||||
// release activeMutex BEFORE evaluating. EvaluateBreak reads signal
|
||||
// memory which can stall (cache miss); holding activeMutex during that
|
||||
// stall would block UpdateBrokersBreakStatus() in the Server thread and
|
||||
// cause unnecessary priority inversion on the RT path.
|
||||
bool shouldCheckBreak = (*anyBreakFlag && !service->IsPaused() &&
|
||||
breakIndices != NULL_PTR(Vec<uint32> *) &&
|
||||
signalInfoPointers != NULL_PTR(DebugSignalInfo **));
|
||||
static const uint32 MAX_BREAK_INDICES = 64u;
|
||||
uint32 localBreakIdx[MAX_BREAK_INDICES];
|
||||
uint32 nb = 0u;
|
||||
if (shouldCheckBreak) {
|
||||
nb = breakIndices->Size();
|
||||
if (nb > MAX_BREAK_INDICES) nb = MAX_BREAK_INDICES;
|
||||
for (uint32 i = 0; i < nb; i++) {
|
||||
uint32 idx = (*breakIndices)[i];
|
||||
localBreakIdx[i] = (*breakIndices)[i];
|
||||
}
|
||||
}
|
||||
|
||||
activeMutex.FastUnLock();
|
||||
|
||||
// Evaluate break conditions outside the lock — safe because
|
||||
// EvaluateBreak only reads signalInfoPointers[idx]->memoryAddress,
|
||||
// which is RT data-bus memory and is never freed during the RT cycle.
|
||||
if (shouldCheckBreak && nb > 0u) {
|
||||
for (uint32 i = 0; i < nb; i++) {
|
||||
uint32 idx = localBreakIdx[i];
|
||||
DebugSignalInfo *s = signalInfoPointers[idx];
|
||||
if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF &&
|
||||
EvaluateBreak(s)) {
|
||||
@@ -145,14 +168,12 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeMutex.FastUnLock();
|
||||
}
|
||||
|
||||
// Pass numCopies explicitly so we can mock it
|
||||
static void
|
||||
InitSignals(BrokerI *broker, DataSourceI &dataSourceIn,
|
||||
DebugService *&service, DebugSignalInfo **&signalInfoPointers,
|
||||
DebugServiceI *&service, DebugSignalInfo **&signalInfoPointers,
|
||||
uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable,
|
||||
const char8 *functionName, SignalDirection direction,
|
||||
volatile bool *anyActiveFlag, Vec<uint32> *activeIndices,
|
||||
@@ -164,16 +185,14 @@ public:
|
||||
signalInfoPointers[i] = NULL_PTR(DebugSignalInfo *);
|
||||
}
|
||||
|
||||
ReferenceContainer *root = ObjectRegistryDatabase::Instance();
|
||||
Reference serviceRef = root->Find("DebugService");
|
||||
if (serviceRef.IsValid()) {
|
||||
service = dynamic_cast<DebugService *>(serviceRef.operator->());
|
||||
}
|
||||
// Use the singleton registered by DebugService::Initialise() — no ORD
|
||||
// search on the Init path, and no dependency on a concrete type.
|
||||
service = DebugServiceI::GetInstance();
|
||||
|
||||
if (service && (copyTable != NULL_PTR(MemoryMapBrokerCopyTableEntry *))) {
|
||||
|
||||
StreamString dsPath;
|
||||
DebugService::GetFullObjectName(dataSourceIn, dsPath);
|
||||
DebugServiceI::GetFullObjectName(dataSourceIn, dsPath);
|
||||
fprintf(stderr, ">> %s broker for %s [%d]\n",
|
||||
direction == InputSignals ? "Input" : "Output", dsPath.Buffer(),
|
||||
numCopies);
|
||||
@@ -225,7 +244,7 @@ public:
|
||||
|
||||
if (gamRef.IsValid()) {
|
||||
StreamString absGamPath;
|
||||
DebugService::GetFullObjectName(*(gamRef.operator->()), absGamPath);
|
||||
DebugServiceI::GetFullObjectName(*(gamRef.operator->()), absGamPath);
|
||||
// Register short path (In/Out) for GUI compatibility
|
||||
gamFullName.Printf("%s.%s.%s", absGamPath.Buffer(), dirStrShort,
|
||||
signalName.Buffer());
|
||||
@@ -261,7 +280,7 @@ public:
|
||||
template <typename BaseClass> class DebugBrokerWrapper : public BaseClass {
|
||||
public:
|
||||
DebugBrokerWrapper() : BaseClass() {
|
||||
service = NULL_PTR(DebugService *);
|
||||
service = NULL_PTR(DebugServiceI *);
|
||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||
numSignals = 0;
|
||||
anyActive = false;
|
||||
@@ -326,7 +345,7 @@ public:
|
||||
return ret;
|
||||
}
|
||||
|
||||
DebugService *service;
|
||||
DebugServiceI *service;
|
||||
DebugSignalInfo **signalInfoPointers;
|
||||
uint32 numSignals;
|
||||
volatile bool anyActive;
|
||||
@@ -343,7 +362,7 @@ template <typename BaseClass>
|
||||
class DebugBrokerWrapperNoOptim : public BaseClass {
|
||||
public:
|
||||
DebugBrokerWrapperNoOptim() : BaseClass() {
|
||||
service = NULL_PTR(DebugService *);
|
||||
service = NULL_PTR(DebugServiceI *);
|
||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||
numSignals = 0;
|
||||
anyActive = false;
|
||||
@@ -386,7 +405,7 @@ public:
|
||||
return ret;
|
||||
}
|
||||
|
||||
DebugService *service;
|
||||
DebugServiceI *service;
|
||||
DebugSignalInfo **signalInfoPointers;
|
||||
uint32 numSignals;
|
||||
volatile bool anyActive;
|
||||
@@ -402,7 +421,7 @@ public:
|
||||
class DebugMemoryMapAsyncOutputBroker : public MemoryMapAsyncOutputBroker {
|
||||
public:
|
||||
DebugMemoryMapAsyncOutputBroker() : MemoryMapAsyncOutputBroker() {
|
||||
service = NULL_PTR(DebugService *);
|
||||
service = NULL_PTR(DebugServiceI *);
|
||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||
numSignals = 0;
|
||||
anyActive = false;
|
||||
@@ -446,7 +465,7 @@ public:
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
DebugService *service;
|
||||
DebugServiceI *service;
|
||||
DebugSignalInfo **signalInfoPointers;
|
||||
uint32 numSignals;
|
||||
volatile bool anyActive;
|
||||
@@ -463,7 +482,7 @@ class DebugMemoryMapAsyncTriggerOutputBroker
|
||||
public:
|
||||
DebugMemoryMapAsyncTriggerOutputBroker()
|
||||
: MemoryMapAsyncTriggerOutputBroker() {
|
||||
service = NULL_PTR(DebugService *);
|
||||
service = NULL_PTR(DebugServiceI *);
|
||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||
numSignals = 0;
|
||||
anyActive = false;
|
||||
@@ -506,7 +525,7 @@ public:
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
DebugService *service;
|
||||
DebugServiceI *service;
|
||||
DebugSignalInfo **signalInfoPointers;
|
||||
uint32 numSignals;
|
||||
volatile bool anyActive;
|
||||
|
||||
@@ -114,7 +114,19 @@ public:
|
||||
ReadFromBuffer(&tempRead, &tempSize, 4);
|
||||
|
||||
if (tempSize > maxSize) {
|
||||
readIndex = write;
|
||||
// FIX #5: Skip only the current entry rather than discarding the
|
||||
// entire ring buffer. tempRead is already past the 16-byte header;
|
||||
// advancing by tempSize lands at the start of the next entry.
|
||||
//
|
||||
// Safety fallback: if tempSize >= bufferSize the stored size field
|
||||
// is corrupt (it can never be that large). In that case we cannot
|
||||
// locate the next entry safely, so fall back to discarding everything
|
||||
// to avoid reading garbage as sample headers on future Pop() calls.
|
||||
if (tempSize >= bufferSize) {
|
||||
readIndex = write; // corrupt ring — discard all
|
||||
} else {
|
||||
readIndex = (tempRead + tempSize) % bufferSize;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "Atomic.h"
|
||||
#include "BasicTCPSocket.h"
|
||||
#include "ClassRegistryItem.h"
|
||||
#include "ConfigurationDatabase.h"
|
||||
@@ -21,7 +22,8 @@
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
DebugService *DebugService::instance = (DebugService *)0;
|
||||
// DebugServiceI static members — defined here so no extra .cpp is needed.
|
||||
DebugServiceI *DebugServiceI::instance = NULL_PTR(DebugServiceI *);
|
||||
|
||||
static void EscapeJson(const char8 *src, StreamString &dst) {
|
||||
if (src == NULL_PTR(const char8 *))
|
||||
@@ -85,6 +87,15 @@ static bool FindPathInContainer(ReferenceContainer *container,
|
||||
|
||||
CLASS_REGISTER(DebugService, "1.0")
|
||||
|
||||
// Out-of-class definitions required by C++98 when the constants are odr-used
|
||||
// (i.e. their address is taken or they appear in a context that needs linkage).
|
||||
const uint32 DebugService::STREAMER_MTU;
|
||||
const uint32 DebugService::STREAMER_BUFFER_SIZE;
|
||||
const uint32 DebugService::CMD_RATE_LIMIT;
|
||||
const uint32 DebugService::CLIENT_IDLE_TIMEOUT_MS;
|
||||
const uint32 DebugService::GET_VALUE_MAX_ELEMENTS;
|
||||
const uint32 DebugService::INPUT_BUFFER_MAX;
|
||||
|
||||
DebugService::DebugService()
|
||||
: ReferenceContainer(), EmbeddedServiceMethodBinderI(),
|
||||
binderServer(this, ServiceBinder::ServerType),
|
||||
@@ -102,11 +113,15 @@ DebugService::DebugService()
|
||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||
streamerPacketOffset = 0u;
|
||||
streamerSequenceNumber = 0u;
|
||||
cmdCountInWindow = 0u;
|
||||
cmdWindowStartMs = 0u;
|
||||
lastDataTimeMs = 0u;
|
||||
inputBuffer = ""; // FIX #10: initialise carry-over buffer
|
||||
}
|
||||
|
||||
DebugService::~DebugService() {
|
||||
if (instance == this) {
|
||||
instance = NULL_PTR(DebugService *);
|
||||
if (DebugServiceI::GetInstance() == this) {
|
||||
DebugServiceI::SetInstance(NULL_PTR(DebugServiceI *));
|
||||
}
|
||||
threadService.Stop();
|
||||
streamerService.Stop();
|
||||
@@ -135,7 +150,7 @@ bool DebugService::Initialise(StructuredDataI &data) {
|
||||
|
||||
if (controlPort > 0) {
|
||||
isServer = true;
|
||||
instance = this;
|
||||
DebugServiceI::SetInstance(this);
|
||||
}
|
||||
|
||||
port = 8081;
|
||||
@@ -463,12 +478,28 @@ void DebugService::ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
|
||||
memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size);
|
||||
}
|
||||
if (signalInfo->isTracing) {
|
||||
if (signalInfo->decimationCounter == 0) {
|
||||
traceBuffer.Push(signalInfo->internalID, timestamp,
|
||||
(uint8 *)signalInfo->memoryAddress, size);
|
||||
// FIX #4: decimationCounter read-modify-write is a data race when multiple
|
||||
// RT threads call ProcessSignal() concurrently for signals with the same
|
||||
// DebugSignalInfo (e.g. an output signal read by two GAMs).
|
||||
//
|
||||
// Pattern: atomically increment, then claim the "push token" by exchanging
|
||||
// back to zero only when the counter reaches the decimation factor.
|
||||
// Only the thread that observes old >= decimationFactor actually pushes;
|
||||
// all others just increment and return. No separate lock is needed for
|
||||
// the counter itself.
|
||||
//
|
||||
// tracePushMutex still serialises the Push() call (FIX #2) because
|
||||
// TraceRingBuffer is SPSC and multiple RT threads can be pushing concurrently.
|
||||
Atomic::Add((volatile int32 *)&signalInfo->decimationCounter, 1);
|
||||
if ((uint32)signalInfo->decimationCounter >= signalInfo->decimationFactor) {
|
||||
int32 old = Atomic::Exchange((volatile int32 *)&signalInfo->decimationCounter, 0);
|
||||
if ((uint32)old >= signalInfo->decimationFactor) {
|
||||
tracePushMutex.FastLock();
|
||||
traceBuffer.Push(signalInfo->internalID, timestamp,
|
||||
(uint8 *)signalInfo->memoryAddress, size);
|
||||
tracePushMutex.FastUnLock();
|
||||
}
|
||||
}
|
||||
signalInfo->decimationCounter =
|
||||
(signalInfo->decimationCounter + 1) % signalInfo->decimationFactor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,6 +532,11 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers,
|
||||
|
||||
void DebugService::UpdateBrokersActiveStatus() {
|
||||
for (uint32 i = 0; i < brokers.Size(); i++) {
|
||||
// FIX #3: signalPointers may be NULL when numSignals > 0 if the broker was
|
||||
// registered with a null array (e.g. from unit tests or misconfigured brokers).
|
||||
// Dereferencing NULL would be UB; skip the broker entirely.
|
||||
if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue;
|
||||
|
||||
uint32 count = 0;
|
||||
for (uint32 j = 0; j < brokers[i].numSignals; j++) {
|
||||
DebugSignalInfo *s = brokers[i].signalPointers[j];
|
||||
@@ -524,20 +560,28 @@ void DebugService::UpdateBrokersActiveStatus() {
|
||||
if (brokers[i].activeMutex)
|
||||
brokers[i].activeMutex->FastLock();
|
||||
|
||||
// FIX #2: Use O(1) Swap instead of operator= (heap alloc + copy) inside
|
||||
// the critical section. The old arrays are carried out in tempInd/tempSizes
|
||||
// and freed after the lock is released.
|
||||
if (brokers[i].activeIndices)
|
||||
*(brokers[i].activeIndices) = tempInd;
|
||||
brokers[i].activeIndices->Swap(tempInd);
|
||||
if (brokers[i].activeSizes)
|
||||
*(brokers[i].activeSizes) = tempSizes;
|
||||
brokers[i].activeSizes->Swap(tempSizes);
|
||||
if (brokers[i].anyActiveFlag)
|
||||
*(brokers[i].anyActiveFlag) = (count > 0);
|
||||
|
||||
if (brokers[i].activeMutex)
|
||||
brokers[i].activeMutex->FastUnLock();
|
||||
// tempInd and tempSizes now hold the old arrays and are freed here,
|
||||
// outside the lock.
|
||||
}
|
||||
}
|
||||
|
||||
void DebugService::UpdateBrokersBreakStatus() {
|
||||
for (uint32 i = 0; i < brokers.Size(); i++) {
|
||||
// FIX #3: same null guard as UpdateBrokersActiveStatus.
|
||||
if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue;
|
||||
|
||||
Vec<uint32> tempBreak;
|
||||
uint32 count = 0;
|
||||
for (uint32 j = 0; j < brokers[i].numSignals; j++) {
|
||||
@@ -549,12 +593,14 @@ void DebugService::UpdateBrokersBreakStatus() {
|
||||
}
|
||||
if (brokers[i].activeMutex)
|
||||
brokers[i].activeMutex->FastLock();
|
||||
// FIX #2: O(1) Swap — old array freed after the lock is released.
|
||||
if (brokers[i].breakIndices)
|
||||
*(brokers[i].breakIndices) = tempBreak;
|
||||
brokers[i].breakIndices->Swap(tempBreak);
|
||||
if (brokers[i].anyBreakFlag)
|
||||
*(brokers[i].anyBreakFlag) = (count > 0);
|
||||
if (brokers[i].activeMutex)
|
||||
brokers[i].activeMutex->FastUnLock();
|
||||
// tempBreak freed here, outside the lock.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,50 +628,143 @@ ErrorManagement::ErrorType DebugService::Server(ExecutionInfo &info) {
|
||||
// The MARTe2 framework calls Execute() in a loop; each call should do
|
||||
// one unit of work and return so the framework can check for Stop().
|
||||
// This replaces the old internal infinite-while pattern.
|
||||
//
|
||||
// FIX #2: activeClient is guarded by clientMutex.
|
||||
// Rule: lock clientMutex only when ASSIGNING the pointer (including to NULL).
|
||||
// Do NOT hold it across blocking I/O (Read/Write) — that would stall any future
|
||||
// thread trying to grab the lock to check whether there is a live client.
|
||||
// The command handler (HandleCommand) receives the pointer by value so it is
|
||||
// safe to call without holding the lock.
|
||||
// Helper: current time in milliseconds (used for rate limiting and idle timeout)
|
||||
uint64 nowMs = (uint64)((float64)HighResolutionTimer::Counter() *
|
||||
HighResolutionTimer::Period() * 1000.0);
|
||||
|
||||
if (activeClient == NULL_PTR(BasicTCPSocket *)) {
|
||||
// Wait briefly for a new connection; return so the framework loop can
|
||||
// check if Stop() was requested between calls.
|
||||
BasicTCPSocket *newClient = tcpServer.WaitConnection(TimeoutType(100));
|
||||
if (newClient != NULL_PTR(BasicTCPSocket *)) {
|
||||
activeClient = newClient;
|
||||
clientMutex.FastLock();
|
||||
activeClient = newClient; // publish pointer — visible to other threads after unlock
|
||||
clientMutex.FastUnLock();
|
||||
// FIX #6/#8: reset per-connection state when a new client connects
|
||||
cmdCountInWindow = 0u;
|
||||
cmdWindowStartMs = nowMs;
|
||||
lastDataTimeMs = nowMs;
|
||||
}
|
||||
} else {
|
||||
// Check if client is still connected
|
||||
if (!activeClient->IsConnected()) {
|
||||
// FIX #8: idle-timeout check — runs every Execute() invocation even when
|
||||
// no data arrives, so a client that half-sends a command and goes silent
|
||||
// cannot hold the slot open indefinitely.
|
||||
if (nowMs - lastDataTimeMs > CLIENT_IDLE_TIMEOUT_MS) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"Server: TCP client idle for >%u ms — closing connection.", CLIENT_IDLE_TIMEOUT_MS);
|
||||
inputBuffer = ""; // FIX #10: discard carry-over on disconnect
|
||||
clientMutex.FastLock();
|
||||
activeClient->Close();
|
||||
delete activeClient;
|
||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||
clientMutex.FastUnLock();
|
||||
cmdCountInWindow = 0u;
|
||||
} else if (!activeClient->IsConnected()) {
|
||||
// Check if client is still connected
|
||||
inputBuffer = ""; // FIX #10
|
||||
clientMutex.FastLock();
|
||||
activeClient->Close();
|
||||
delete activeClient;
|
||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||
clientMutex.FastUnLock();
|
||||
} else {
|
||||
char buffer[1024];
|
||||
uint32 size = 1024;
|
||||
if (activeClient->Read(buffer, size)) {
|
||||
if (size > 0) {
|
||||
// Process each line separately
|
||||
char *ptr = buffer;
|
||||
char *end = buffer + size;
|
||||
while (ptr < end) {
|
||||
char *newline = (char *)memchr(ptr, '\n', end - ptr);
|
||||
if (!newline) {
|
||||
break;
|
||||
lastDataTimeMs = nowMs; // FIX #8: refresh idle timestamp
|
||||
|
||||
// FIX #6: slide rate-limit window
|
||||
if (nowMs - cmdWindowStartMs >= 1000u) {
|
||||
cmdWindowStartMs = nowMs;
|
||||
cmdCountInWindow = 0u;
|
||||
}
|
||||
|
||||
// FIX #10: guard against a client that never sends a newline,
|
||||
// which would grow inputBuffer without bound.
|
||||
if (inputBuffer.Size() + (uint32)size > INPUT_BUFFER_MAX) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"Server: input buffer overflow (>%u bytes without newline) "
|
||||
"— disconnecting.", INPUT_BUFFER_MAX);
|
||||
inputBuffer = "";
|
||||
clientMutex.FastLock();
|
||||
activeClient->Close();
|
||||
delete activeClient;
|
||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||
clientMutex.FastUnLock();
|
||||
cmdCountInWindow = 0u;
|
||||
} else {
|
||||
// FIX #10: accumulate data; parse only complete (newline-terminated)
|
||||
// commands so that commands split across TCP segments are assembled
|
||||
// correctly before dispatch.
|
||||
(void)inputBuffer.Seek(inputBuffer.Size());
|
||||
uint32 writeSize = (uint32)size;
|
||||
inputBuffer.Write(buffer, writeSize);
|
||||
|
||||
const char8 *raw = inputBuffer.Buffer();
|
||||
uint32 total = (uint32)inputBuffer.Size();
|
||||
uint32 lineStart = 0u;
|
||||
bool rateLimitExceeded = false;
|
||||
|
||||
for (uint32 pos = 0u; pos < total && !rateLimitExceeded; pos++) {
|
||||
if (raw[pos] != '\n') continue;
|
||||
|
||||
uint32 len = pos - lineStart;
|
||||
// Trim trailing CR
|
||||
if (len > 0u && raw[lineStart + len - 1u] == '\r') len--;
|
||||
|
||||
if (len > 0u) {
|
||||
cmdCountInWindow++;
|
||||
if (cmdCountInWindow > CMD_RATE_LIMIT) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"Server: client exceeded rate limit (%u cmd/s) — disconnecting.",
|
||||
CMD_RATE_LIMIT);
|
||||
rateLimitExceeded = true;
|
||||
break;
|
||||
}
|
||||
StreamString command;
|
||||
uint32 cmdLen = len;
|
||||
command.Write(raw + lineStart, cmdLen);
|
||||
HandleCommand(command, activeClient);
|
||||
}
|
||||
lineStart = pos + 1u;
|
||||
}
|
||||
*newline = '\0';
|
||||
// Skip carriage return if present
|
||||
if (newline > ptr && *(newline - 1) == '\r')
|
||||
*(newline - 1) = '\0';
|
||||
StreamString command;
|
||||
uint32 len = (uint32)(newline - ptr);
|
||||
command.Write(ptr, len);
|
||||
if (command.Size() > 0) {
|
||||
HandleCommand(command, activeClient);
|
||||
|
||||
if (rateLimitExceeded) {
|
||||
inputBuffer = "";
|
||||
clientMutex.FastLock();
|
||||
activeClient->Close();
|
||||
delete activeClient;
|
||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||
clientMutex.FastUnLock();
|
||||
cmdCountInWindow = 0u;
|
||||
} else {
|
||||
// Save the incomplete line (bytes after the last '\n') for the
|
||||
// next Read() — they form the start of the next command.
|
||||
StreamString newInputBuffer;
|
||||
if (lineStart < total) {
|
||||
uint32 remLen = total - lineStart;
|
||||
newInputBuffer.Write(raw + lineStart, remLen);
|
||||
}
|
||||
inputBuffer = newInputBuffer;
|
||||
}
|
||||
ptr = newline + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Read failed (client disconnected or error), clean up
|
||||
inputBuffer = ""; // FIX #10
|
||||
clientMutex.FastLock();
|
||||
activeClient->Close();
|
||||
delete activeClient;
|
||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||
clientMutex.FastUnLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,19 +793,63 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
|
||||
HighResolutionTimer::Period() * 1000000.0);
|
||||
void *address = NULL_PTR(void *);
|
||||
if (monitoredSignals[i].dataSource->GetSignalMemoryBuffer(monitoredSignals[i].signalIdx, 0, address)) {
|
||||
// FIX #11: ProcessSignal() (called from RT broker threads) serialises
|
||||
// its traceBuffer.Push() under tracePushMutex. The Streamer's monitored-
|
||||
// signal path previously skipped that lock, creating a multi-producer race
|
||||
// between the RT threads and the Streamer thread.
|
||||
//
|
||||
// Lock order: mutex (always held here) → tracePushMutex.
|
||||
// RT broker threads only ever acquire tracePushMutex (never mutex), so
|
||||
// this ordering introduces no deadlock risk.
|
||||
tracePushMutex.FastLock();
|
||||
traceBuffer.Push(monitoredSignals[i].internalID, ts, (uint8 *)address, monitoredSignals[i].size);
|
||||
tracePushMutex.FastUnLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
mutex.FastUnLock();
|
||||
|
||||
// Drain ring buffer into UDP packet(s)
|
||||
// Drain ring buffer into UDP packet(s).
|
||||
//
|
||||
// FIX #1: Two-level bounds checking to prevent buffer overflow.
|
||||
//
|
||||
// Level 1 — per-sample maximum size:
|
||||
// sampleData is SAMPLE_BUF_SIZE bytes. Pop() enforces this via maxSize so
|
||||
// the local buffer can never be overrun by Pop() itself.
|
||||
//
|
||||
// Level 2 — assembly-buffer hard limit:
|
||||
// After a flush, streamerPacketOffset resets to sizeof(TraceHeader). The
|
||||
// next sample occupies (sizeof(TraceHeader) + 16 + size) bytes. With
|
||||
// SAMPLE_BUF_SIZE = 1024 that is at most ~1060 bytes — well within
|
||||
// STREAMER_BUFFER_SIZE = 4096. The explicit guard below catches the case
|
||||
// where SAMPLE_BUF_SIZE or STREAMER_MTU are later changed carelessly so
|
||||
// that a single sample could still overflow the buffer.
|
||||
static const uint32 SAMPLE_BUF_SIZE = 1024u;
|
||||
// Compile-time sanity: one full header + per-sample header + max sample must fit
|
||||
// inside the assembly buffer. If this fires, raise STREAMER_BUFFER_SIZE or
|
||||
// lower SAMPLE_BUF_SIZE.
|
||||
// (C++98-compatible static assert via sizeof a negative-size array)
|
||||
typedef char StaticAssert_StreamerBufferTooSmall[
|
||||
(sizeof(TraceHeader) + 16u + SAMPLE_BUF_SIZE <= STREAMER_BUFFER_SIZE) ? 1 : -1];
|
||||
(void)sizeof(StaticAssert_StreamerBufferTooSmall); // suppress unused-typedef warning
|
||||
|
||||
uint32 id, size;
|
||||
uint64 ts;
|
||||
uint8 sampleData[1024];
|
||||
uint8 sampleData[SAMPLE_BUF_SIZE];
|
||||
bool hasData = false;
|
||||
while (traceBuffer.Pop(id, ts, sampleData, size, 1024)) {
|
||||
while (traceBuffer.Pop(id, ts, sampleData, size, SAMPLE_BUF_SIZE)) {
|
||||
hasData = true;
|
||||
|
||||
// Level 2 guard — should never fire given the static assert above, but
|
||||
// defends against future changes that widen SAMPLE_BUF_SIZE or shrink
|
||||
// STREAMER_BUFFER_SIZE without updating the other.
|
||||
if (size > SAMPLE_BUF_SIZE || streamerPacketOffset + 16u + size > STREAMER_BUFFER_SIZE) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"Streamer: sample size %u would overflow assembly buffer (%u bytes used of %u) "
|
||||
"— sample dropped.", size, streamerPacketOffset, STREAMER_BUFFER_SIZE);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (streamerPacketOffset == 0u) {
|
||||
TraceHeader header;
|
||||
header.magic = 0xDA7A57AD;
|
||||
@@ -676,7 +859,8 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
|
||||
memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader));
|
||||
streamerPacketOffset = sizeof(TraceHeader);
|
||||
}
|
||||
if (streamerPacketOffset + 16u + size > 1400u) {
|
||||
// Flush the current packet when adding this sample would exceed the MTU.
|
||||
if (streamerPacketOffset + 16u + size > STREAMER_MTU) {
|
||||
uint32 toWrite = streamerPacketOffset;
|
||||
(void)udpSocket.Write((char8 *)streamerPacketBuffer, toWrite);
|
||||
TraceHeader header;
|
||||
@@ -705,8 +889,8 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
|
||||
return ErrorManagement::NoError;
|
||||
}
|
||||
|
||||
bool DebugService::GetFullObjectName(const Object &obj,
|
||||
StreamString &fullPath) {
|
||||
bool DebugServiceI::GetFullObjectName(const Object &obj,
|
||||
StreamString &fullPath) {
|
||||
fullPath = "";
|
||||
if (FindPathInContainer(ObjectRegistryDatabase::Instance(), &obj, fullPath)) {
|
||||
return true;
|
||||
@@ -893,15 +1077,42 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) {
|
||||
const char8 *eq = StringHelper::SearchChar(line.Buffer(), '=');
|
||||
if (eq != NULL_PTR(const char8 *)) {
|
||||
uint32 eqPos = (uint32)(eq - line.Buffer());
|
||||
(void)line.Seek(0u);
|
||||
|
||||
char8 keyBuf[256] = {'\0'};
|
||||
// FIX #7: Enforce buffer bounds before reading key/value.
|
||||
// A key longer than keyBuf would be silently truncated by
|
||||
// Read(), producing a mismatched key (e.g. "LongKe" instead
|
||||
// of "LongKey") that causes CDB.Write() to inject a garbled
|
||||
// parameter. Skip the entire line if the key overflows —
|
||||
// the CDB write would be nonsensical anyway.
|
||||
// Values are safely capped: the worst case is a truncated
|
||||
// value, which the target GAM can detect and reject.
|
||||
static const uint32 KEY_BUF_SIZE = 256u;
|
||||
static const uint32 VAL_BUF_SIZE = 1024u;
|
||||
if (eqPos >= KEY_BUF_SIZE) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"MSG: key length %u exceeds buffer (%u) — line skipped.",
|
||||
eqPos, KEY_BUF_SIZE);
|
||||
line = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)line.Seek(0u);
|
||||
char8 keyBuf[KEY_BUF_SIZE];
|
||||
MemoryOperationsHelper::Set(keyBuf, '\0', KEY_BUF_SIZE);
|
||||
uint32 keyReadSize = eqPos;
|
||||
(void)line.Read(keyBuf, keyReadSize);
|
||||
|
||||
(void)line.Seek(eqPos + 1u);
|
||||
uint32 valLen = (uint32)(line.Size() - eqPos - 1u);
|
||||
char8 valBuf[1024] = {'\0'};
|
||||
// Truncate silently to VAL_BUF_SIZE - 1 (leave room for '\0')
|
||||
if (valLen >= VAL_BUF_SIZE) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"MSG: value length %u truncated to %u for key '%s'.",
|
||||
valLen, VAL_BUF_SIZE - 1u, keyBuf);
|
||||
valLen = VAL_BUF_SIZE - 1u;
|
||||
}
|
||||
char8 valBuf[VAL_BUF_SIZE];
|
||||
MemoryOperationsHelper::Set(valBuf, '\0', VAL_BUF_SIZE);
|
||||
(void)line.Read(valBuf, valLen);
|
||||
|
||||
// Trim trailing whitespace from value
|
||||
@@ -1418,6 +1629,25 @@ uint32 DebugService::ForceSignal(const char8 *name, const char8 *valueStr) {
|
||||
SuffixMatch(aliases[i].name.Buffer(), name)) {
|
||||
|
||||
DebugSignalInfo *s = signals[aliases[i].signalIndex];
|
||||
|
||||
// FIX #4: Guard against writing past the end of forcedValue[1024].
|
||||
// TypeConvert writes (byteSize * numberOfElements) bytes into forcedValue.
|
||||
// For large arrays (e.g. 512 float64s = 4096 bytes) this would silently
|
||||
// overflow the 1024-byte buffer and corrupt adjacent struct members.
|
||||
//
|
||||
// TypeDescriptor::numberOfBits is the element size in bits; integer divide
|
||||
// by 8 gives bytes. For structured/opaque types numberOfBits may be 0 —
|
||||
// those are also skipped because we cannot determine the layout safely.
|
||||
uint32 elemBytes = (uint32)(s->type.numberOfBits) / 8u;
|
||||
uint32 totalBytes = elemBytes * s->numberOfElements;
|
||||
if (elemBytes == 0u || totalBytes > (uint32)sizeof(s->forcedValue)) {
|
||||
REPORT_ERROR_STATIC(ErrorManagement::Warning,
|
||||
"ForceSignal: signal '%s' requires %u bytes but forcedValue buffer is "
|
||||
"only %u bytes — force request ignored.",
|
||||
s->name.Buffer(), totalBytes, (uint32)sizeof(s->forcedValue));
|
||||
continue;
|
||||
}
|
||||
|
||||
s->isForcing = true;
|
||||
AnyType dest(s->type, 0u, s->forcedValue);
|
||||
AnyType source(CharString, 0u, valueStr);
|
||||
@@ -1536,7 +1766,18 @@ void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) {
|
||||
TypeDescriptor td = sig->type;
|
||||
uint32 nElem = sig->numberOfElements;
|
||||
uint32 byteSize = (td.numberOfBits > 0u) ? (td.numberOfBits / 8u) : 1u;
|
||||
|
||||
// FIX #9: cap element count before computing byte totals.
|
||||
// Without a cap, a 1M-element uint8 signal would produce a multi-MB
|
||||
// comma-separated response, exhausting heap and blocking the Server thread.
|
||||
// GET_VALUE_MAX_ELEMENTS = 256 gives a ~4 KB worst-case JSON string.
|
||||
bool truncated = (nElem > GET_VALUE_MAX_ELEMENTS);
|
||||
if (truncated) {
|
||||
nElem = GET_VALUE_MAX_ELEMENTS;
|
||||
}
|
||||
uint32 totalBytes = byteSize * nElem;
|
||||
// Secondary cap: even after the element limit, ensure we never read more
|
||||
// than 1024 bytes from the RT memory region.
|
||||
if (totalBytes > 1024u) { totalBytes = 1024u; nElem = totalBytes / byteSize; }
|
||||
// Copy bytes while holding the mutex to avoid data races with the RT thread
|
||||
uint8 localBuf[1024];
|
||||
@@ -1582,7 +1823,10 @@ void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) {
|
||||
vp++;
|
||||
}
|
||||
resp += "\", \"Elements\": ";
|
||||
resp.Printf("%u}\nOK VALUE\n", nElem);
|
||||
// Include the capped element count and a Truncated flag so the client can
|
||||
// distinguish "this is the full signal" from "there are more elements".
|
||||
resp.Printf("%u, \"Truncated\": %s}\nOK VALUE\n",
|
||||
nElem, truncated ? "true" : "false");
|
||||
uint32 s = resp.Size();
|
||||
(void)client->Write(resp.Buffer(), s);
|
||||
}
|
||||
|
||||
@@ -4,45 +4,30 @@
|
||||
#include "BasicTCPSocket.h"
|
||||
#include "BasicUDPSocket.h"
|
||||
#include "ConfigurationDatabase.h"
|
||||
#include "DebugCore.h"
|
||||
#include "DebugServiceI.h"
|
||||
#include "EmbeddedServiceMethodBinderI.h"
|
||||
#include "MessageI.h"
|
||||
#include "Object.h"
|
||||
#include "ReferenceContainer.h"
|
||||
#include "ReferenceT.h"
|
||||
#include "SingleThreadService.h"
|
||||
#include "StreamString.h"
|
||||
#include "Vec.h"
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
class MemoryMapBroker;
|
||||
class DataSourceI;
|
||||
|
||||
struct SignalAlias {
|
||||
StreamString name;
|
||||
uint32 signalIndex;
|
||||
};
|
||||
|
||||
struct BrokerInfo {
|
||||
DebugSignalInfo **signalPointers;
|
||||
uint32 numSignals;
|
||||
MemoryMapBroker *broker;
|
||||
volatile bool *anyActiveFlag;
|
||||
Vec<uint32> *activeIndices;
|
||||
Vec<uint32> *activeSizes;
|
||||
FastPollingMutexSem *activeMutex;
|
||||
// Conditional break — mirrors activeIndices but for break-enabled signals
|
||||
volatile bool *anyBreakFlag;
|
||||
Vec<uint32> *breakIndices;
|
||||
// GAM association for step-by-GAM
|
||||
StreamString gamName;
|
||||
bool isOutput;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief TCP/UDP implementation of DebugServiceI.
|
||||
*
|
||||
* Provides signal tracing (UDP), forced-value injection, break conditions,
|
||||
* and a text command channel (TCP) with a log-forwarding sidecar (TcpLogger).
|
||||
* Alternative transports (TTY, WebSocket, …) can be added by implementing
|
||||
* DebugServiceI without touching this class or any broker wrapper code.
|
||||
*/
|
||||
class DebugService : public ReferenceContainer,
|
||||
public MessageI,
|
||||
public EmbeddedServiceMethodBinderI {
|
||||
public EmbeddedServiceMethodBinderI,
|
||||
public DebugServiceI {
|
||||
public:
|
||||
friend class DebugServiceTest;
|
||||
CLASS_REGISTER_DECLARATION()
|
||||
@@ -52,49 +37,43 @@ public:
|
||||
|
||||
virtual bool Initialise(StructuredDataI &data);
|
||||
|
||||
DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type,
|
||||
const char8 *name, uint8 numberOfDimensions = 0,
|
||||
uint32 numberOfElements = 1);
|
||||
void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
|
||||
uint64 timestamp);
|
||||
// DebugServiceI RT-path overrides
|
||||
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type,
|
||||
const char8 *name,
|
||||
uint8 numberOfDimensions = 0,
|
||||
uint32 numberOfElements = 1);
|
||||
virtual void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
|
||||
uint64 timestamp);
|
||||
virtual void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals,
|
||||
MemoryMapBroker *broker, volatile bool *anyActiveFlag,
|
||||
Vec<uint32> *activeIndices, Vec<uint32> *activeSizes,
|
||||
FastPollingMutexSem *activeMutex,
|
||||
volatile bool *anyBreakFlag, Vec<uint32> *breakIndices,
|
||||
const char8 *gamName = NULL_PTR(const char8 *),
|
||||
bool isOutput = false);
|
||||
virtual bool IsPaused() const { return isPaused; }
|
||||
virtual void SetPaused(bool paused) { isPaused = paused; }
|
||||
virtual bool IsStepPending() const { return stepRemaining > 0u; }
|
||||
virtual void ConsumeStepIfNeeded(const char8 *gamName,
|
||||
const char8 *threadName = NULL_PTR(const char8 *));
|
||||
|
||||
void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals,
|
||||
MemoryMapBroker *broker, volatile bool *anyActiveFlag,
|
||||
Vec<uint32> *activeIndices, Vec<uint32> *activeSizes,
|
||||
FastPollingMutexSem *activeMutex,
|
||||
volatile bool *anyBreakFlag, Vec<uint32> *breakIndices,
|
||||
const char8 *gamName = NULL_PTR(const char8 *),
|
||||
bool isOutput = false);
|
||||
// DebugServiceI control-path overrides
|
||||
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr);
|
||||
virtual uint32 UnforceSignal(const char8 *name);
|
||||
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1);
|
||||
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold);
|
||||
virtual uint32 ClearBreak (const char8 *name);
|
||||
virtual bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
|
||||
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs);
|
||||
virtual uint32 UnmonitorSignal (const char8 *path);
|
||||
|
||||
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
|
||||
|
||||
virtual ErrorManagement::ErrorType HandleMessage(ReferenceT<Message> &data);
|
||||
|
||||
bool IsPaused() const { return isPaused; }
|
||||
void SetPaused(bool paused) { isPaused = paused; }
|
||||
|
||||
// Step-by-GAM / per-thread: called by each output broker.
|
||||
// threadName is the OS thread name (from Threads::Name(Threads::Id())).
|
||||
// Decrements stepRemaining only when stepThreadFilter is empty or matches;
|
||||
// when stepRemaining reaches 0, sets isPaused = true and records gamName.
|
||||
void ConsumeStepIfNeeded(const char8 *gamName,
|
||||
const char8 *threadName = NULL_PTR(const char8 *));
|
||||
|
||||
// TCP-transport-specific methods (not part of DebugServiceI)
|
||||
void GetStepStatus(BasicTCPSocket *client);
|
||||
// Step n cycles; if threadName is non-null/non-empty, only that thread advances.
|
||||
void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *));
|
||||
|
||||
// Read the current raw value of a signal from its memory address.
|
||||
void GetSignalValue(const char8 *name, BasicTCPSocket *client);
|
||||
|
||||
static bool GetFullObjectName(const Object &obj, StreamString &fullPath);
|
||||
|
||||
uint32 ForceSignal(const char8 *name, const char8 *valueStr);
|
||||
uint32 UnforceSignal(const char8 *name);
|
||||
uint32 TraceSignal(const char8 *name, bool enable, uint32 decimation = 1);
|
||||
uint32 SetBreak(const char8 *name, uint8 op, float64 threshold);
|
||||
uint32 ClearBreak(const char8 *name);
|
||||
bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
|
||||
void Discover(BasicTCPSocket *client);
|
||||
void InfoNode(const char8 *path, BasicTCPSocket *client);
|
||||
void ListNodes(const char8 *path, BasicTCPSocket *client);
|
||||
@@ -112,9 +91,6 @@ public:
|
||||
StreamString path;
|
||||
};
|
||||
|
||||
uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs);
|
||||
uint32 UnmonitorSignal(const char8 *path);
|
||||
|
||||
private:
|
||||
void HandleCommand(StreamString cmd, BasicTCPSocket *client);
|
||||
void UpdateBrokersActiveStatus();
|
||||
@@ -178,17 +154,140 @@ private:
|
||||
FastPollingMutexSem mutex;
|
||||
TraceRingBuffer traceBuffer;
|
||||
|
||||
/**
|
||||
* @brief Mutex protecting traceBuffer.Push() from concurrent RT broker threads.
|
||||
*
|
||||
* TraceRingBuffer is designed for single-producer / single-consumer access.
|
||||
* In practice, multiple RT threads (one per RealTimeThread) call ProcessSignal()
|
||||
* concurrently, making it a multi-producer scenario. Without a lock they can
|
||||
* both pass the free-space check, both pick the same write index, and corrupt
|
||||
* each other's samples. This mutex serialises the Push path.
|
||||
*
|
||||
* The Streamer thread (sole consumer) never holds this lock, so the lock is
|
||||
* only contended between RT threads — not between RT and Streamer.
|
||||
*
|
||||
* FIX #2 — TraceRingBuffer multi-producer race.
|
||||
*/
|
||||
FastPollingMutexSem tracePushMutex;
|
||||
|
||||
/**
|
||||
* @brief Mutex protecting the activeClient pointer.
|
||||
*
|
||||
* activeClient is currently owned exclusively by the Server thread:
|
||||
* it is created, read, and destroyed only inside Server(). The mutex
|
||||
* enforces this invariant and makes future cross-thread access safe.
|
||||
*
|
||||
* Rules:
|
||||
* - Hold clientMutex whenever assigning (including to NULL) activeClient.
|
||||
* - Do NOT hold clientMutex across blocking I/O calls (Read / Write)
|
||||
* to avoid starving threads that only need the pointer.
|
||||
* - Any future code that needs to send data to the active client from a
|
||||
* non-Server thread must lock clientMutex, copy the pointer, release
|
||||
* the lock, then do the Write.
|
||||
*
|
||||
* FIX #2 — activeClient cross-thread safety.
|
||||
*/
|
||||
FastPollingMutexSem clientMutex;
|
||||
|
||||
BasicTCPSocket *activeClient;
|
||||
|
||||
/**
|
||||
* Maximum payload bytes in a single UDP datagram (leave room for IP+UDP headers
|
||||
* on a standard 1500-byte Ethernet MTU). Used in the Streamer to decide when
|
||||
* to flush the current packet and start a new one.
|
||||
*
|
||||
* FIX #1 — named constant to replace the magic number 1400.
|
||||
*/
|
||||
static const uint32 STREAMER_MTU = 1400u;
|
||||
|
||||
/**
|
||||
* Size of the streamer assembly buffer. Must be >= STREAMER_MTU so at least
|
||||
* one full UDP payload can be staged before flushing. A sample that on its own
|
||||
* would exceed (STREAMER_BUFFER_SIZE - sizeof(TraceHeader) - 16) bytes is
|
||||
* silently dropped and logged — it can never fit in any packet.
|
||||
*
|
||||
* FIX #1 — named constant; prevents silent integer arithmetic relying on
|
||||
* the array bound being "obviously 4096".
|
||||
*/
|
||||
static const uint32 STREAMER_BUFFER_SIZE = 4096u;
|
||||
|
||||
// Streamer state persisted across Execute() calls (framework loops Execute)
|
||||
uint8 streamerPacketBuffer[4096];
|
||||
uint8 streamerPacketBuffer[STREAMER_BUFFER_SIZE];
|
||||
uint32 streamerPacketOffset;
|
||||
uint32 streamerSequenceNumber;
|
||||
|
||||
/**
|
||||
* @brief Maximum commands allowed per 1-second sliding window per client.
|
||||
*
|
||||
* A client sending more than this many commands in one second is disconnected.
|
||||
* This prevents a tight command loop from monopolising the Server thread and
|
||||
* starving normal usage. The limit is intentionally generous (100/s) so that
|
||||
* legitimate scripted tooling is unaffected.
|
||||
*
|
||||
* FIX #6 — TCP command rate limiting.
|
||||
*/
|
||||
static const uint32 CMD_RATE_LIMIT = 100u;
|
||||
|
||||
/**
|
||||
* @brief Close the active TCP client if no data is received for this many ms.
|
||||
*
|
||||
* A client that sends a partial command and never completes it would otherwise
|
||||
* hold the single active-client slot open indefinitely, blocking all other
|
||||
* connections. After CLIENT_IDLE_TIMEOUT_MS of silence the connection is
|
||||
* closed and the slot freed.
|
||||
*
|
||||
* FIX #8 — active-client idle timeout.
|
||||
*/
|
||||
static const uint32 CLIENT_IDLE_TIMEOUT_MS = 30000u;
|
||||
|
||||
/**
|
||||
* @brief Maximum array elements returned by GetSignalValue / VALUE command.
|
||||
*
|
||||
* Without a cap, a 1-million-element uint8 array would produce a multi-MB
|
||||
* response string, exhausting heap and blocking the Server thread. Elements
|
||||
* beyond this limit are omitted; the response includes a "Truncated" flag.
|
||||
*
|
||||
* FIX #9 — GetSignalValue unbounded array output.
|
||||
*/
|
||||
static const uint32 GET_VALUE_MAX_ELEMENTS = 256u;
|
||||
|
||||
// Rate-limiter state (per active TCP connection, reset on each new connection)
|
||||
uint32 cmdCountInWindow; // commands received in the current 1-second window
|
||||
uint64 cmdWindowStartMs; // start of the current window, in milliseconds
|
||||
|
||||
// Idle-timeout state
|
||||
uint64 lastDataTimeMs; // last time the active client sent any data (ms)
|
||||
|
||||
/**
|
||||
* @brief Carry-over buffer for multi-segment TCP commands.
|
||||
*
|
||||
* A single TCP Read() call may return only part of a command if the OS
|
||||
* delivers it in multiple segments. Bytes after the last '\n' in the
|
||||
* current receive window are saved here and prepended to the next Read().
|
||||
* This ensures that "FORCE Signal " arriving in one segment and "123\n"
|
||||
* arriving in the next are assembled into the single command
|
||||
* "FORCE Signal 123" before dispatch.
|
||||
*
|
||||
* The buffer is bounded by INPUT_BUFFER_MAX (8 KiB). If a client sends
|
||||
* more than that without a newline it is disconnected.
|
||||
*
|
||||
* FIX #10 — incomplete input buffer handling for multi-line commands.
|
||||
*/
|
||||
StreamString inputBuffer;
|
||||
|
||||
/**
|
||||
* @brief Maximum carry-over bytes allowed before a client is disconnected.
|
||||
*
|
||||
* A client that sends a very long line without a newline would otherwise
|
||||
* grow inputBuffer without bound. At 8 KiB the limit is generous enough
|
||||
* for any legitimate command while still protecting against memory exhaustion.
|
||||
*
|
||||
* FIX #10 — DoS guard for the input carry-over buffer.
|
||||
*/
|
||||
static const uint32 INPUT_BUFFER_MAX = 8192u;
|
||||
|
||||
ConfigurationDatabase fullConfig;
|
||||
bool manualConfigSet;
|
||||
|
||||
static DebugService *instance;
|
||||
};
|
||||
|
||||
} // namespace MARTe
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#ifndef DEBUGSERVICEI_H
|
||||
#define DEBUGSERVICEI_H
|
||||
|
||||
/**
|
||||
* @file DebugServiceI.h
|
||||
* @brief Abstract interface for the MARTe2 debug/instrumentation service.
|
||||
*
|
||||
* All broker wrappers (DebugBrokerWrapper) depend solely on this interface,
|
||||
* decoupling them from the concrete TCP/UDP implementation. Alternative
|
||||
* transports — TTY, WebSocket, shared-memory ring, etc. — can be plugged in
|
||||
* by providing a new concrete implementation without touching any broker or
|
||||
* application code.
|
||||
*
|
||||
* The interface is split into two logical groups:
|
||||
*
|
||||
* RT-path API
|
||||
* Called from broker threads on every RT cycle. Implementations must
|
||||
* keep these methods as cheap as possible; the only permissible
|
||||
* synchronisation primitive is a fast spinlock (FastPollingMutexSem).
|
||||
*
|
||||
* Control-path API
|
||||
* Called from server / command-handler threads (TCP, TTY, Web …).
|
||||
* Latency here is acceptable; correctness and thread-safety matter.
|
||||
*
|
||||
* Concrete implementations register themselves during Initialise() via
|
||||
* SetInstance(). Broker wrappers retrieve the active instance via
|
||||
* GetInstance(); there is no ORD search on the hot path.
|
||||
*/
|
||||
|
||||
#include "DebugCore.h"
|
||||
#include "FastPollingMutexSem.h"
|
||||
#include "Object.h"
|
||||
#include "StreamString.h"
|
||||
#include "TypeDescriptor.h"
|
||||
#include "Vec.h"
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
// Forward declarations — concrete types are only needed in the implementation.
|
||||
class MemoryMapBroker;
|
||||
|
||||
struct SignalAlias {
|
||||
StreamString name;
|
||||
uint32 signalIndex;
|
||||
};
|
||||
|
||||
struct BrokerInfo {
|
||||
DebugSignalInfo **signalPointers;
|
||||
uint32 numSignals;
|
||||
MemoryMapBroker *broker;
|
||||
volatile bool *anyActiveFlag;
|
||||
Vec<uint32> *activeIndices;
|
||||
Vec<uint32> *activeSizes;
|
||||
FastPollingMutexSem *activeMutex;
|
||||
volatile bool *anyBreakFlag;
|
||||
Vec<uint32> *breakIndices;
|
||||
StreamString gamName;
|
||||
bool isOutput;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Abstract debug-service interface.
|
||||
*/
|
||||
class DebugServiceI {
|
||||
public:
|
||||
// -------------------------------------------------------------------------
|
||||
// Static instance registry
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Return the currently registered debug-service instance, or NULL.
|
||||
*
|
||||
* Called on every broker Init() path and from OutpautPauseAndStep().
|
||||
* Returns NULL when no debug service has been initialised, in which case
|
||||
* all instrumentation is a no-op.
|
||||
*/
|
||||
static DebugServiceI *GetInstance() { return instance; }
|
||||
|
||||
/**
|
||||
* @brief Register @p inst as the global debug-service.
|
||||
*
|
||||
* Concrete implementations call this from their Initialise() method.
|
||||
* Passing NULL deregisters the current instance (called from the
|
||||
* destructor so dangling pointers are never visible to broker threads).
|
||||
*/
|
||||
static void SetInstance(DebugServiceI *inst) { instance = inst; }
|
||||
|
||||
virtual ~DebugServiceI() {}
|
||||
|
||||
// =========================================================================
|
||||
// RT-path API (called from broker execute threads every cycle)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Register a signal memory region with the debug service.
|
||||
*
|
||||
* Called once per signal during broker Init(). Returns a pointer to the
|
||||
* internal DebugSignalInfo that the broker caches for use on the hot path.
|
||||
* Thread-safe; must not be called after the RT loop has started.
|
||||
*/
|
||||
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress,
|
||||
TypeDescriptor type,
|
||||
const char8 *name,
|
||||
uint8 numberOfDimensions = 0,
|
||||
uint32 numberOfElements = 1) = 0;
|
||||
|
||||
/**
|
||||
* @brief Process one signal on the RT path.
|
||||
*
|
||||
* Applies forced values (memcpy into signal memory) and, when tracing is
|
||||
* enabled and the decimation counter fires, pushes a sample to the trace
|
||||
* ring buffer. Called under the broker's activeMutex; implementations
|
||||
* must not acquire any lock that is also held by the Server thread.
|
||||
*/
|
||||
virtual void ProcessSignal(DebugSignalInfo *signalInfo,
|
||||
uint32 size,
|
||||
uint64 timestamp) = 0;
|
||||
|
||||
/**
|
||||
* @brief Register a broker so the service can push active/break index
|
||||
* updates to it without iterating every signal.
|
||||
*/
|
||||
virtual void RegisterBroker(DebugSignalInfo **signalPointers,
|
||||
uint32 numSignals,
|
||||
MemoryMapBroker *broker,
|
||||
volatile bool *anyActiveFlag,
|
||||
Vec<uint32> *activeIndices,
|
||||
Vec<uint32> *activeSizes,
|
||||
FastPollingMutexSem *activeMutex,
|
||||
volatile bool *anyBreakFlag,
|
||||
Vec<uint32> *breakIndices,
|
||||
const char8 *gamName = NULL_PTR(const char8 *),
|
||||
bool isOutput = false) = 0;
|
||||
|
||||
/** @brief Return true if the RT loop is currently held at a pause/breakpoint. */
|
||||
virtual bool IsPaused() const = 0;
|
||||
|
||||
/** @brief Set or clear the paused state (called by break-condition logic). */
|
||||
virtual void SetPaused(bool paused) = 0;
|
||||
|
||||
/** @brief Return true if a step count is pending (stepRemaining > 0). */
|
||||
virtual bool IsStepPending() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Consume one step credit for the current output-broker cycle.
|
||||
*
|
||||
* Called by every output broker after Execute(). No-op when stepRemaining
|
||||
* is zero (the common case); only acquires a mutex when stepping is active.
|
||||
*/
|
||||
virtual void ConsumeStepIfNeeded(
|
||||
const char8 *gamName,
|
||||
const char8 *threadName = NULL_PTR(const char8 *)) = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Control-path API (called from server / command-handler threads)
|
||||
// =========================================================================
|
||||
|
||||
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr) = 0;
|
||||
virtual uint32 UnforceSignal(const char8 *name) = 0;
|
||||
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1) = 0;
|
||||
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold) = 0;
|
||||
virtual uint32 ClearBreak (const char8 *name) = 0;
|
||||
virtual bool IsInstrumented(const char8 *fullPath,
|
||||
bool &traceable, bool &forcable) = 0;
|
||||
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs) = 0;
|
||||
virtual uint32 UnmonitorSignal (const char8 *path) = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Utility (implementation-agnostic, defined in DebugService.cpp)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @brief Resolve the fully-qualified ORD path of @p obj into @p fullPath.
|
||||
*
|
||||
* Static so it can be called without a service instance. All concrete
|
||||
* implementations (and InitSignals in DebugBrokerWrapper.h) use this
|
||||
* to build canonical signal names. The definition lives in
|
||||
* DebugService.cpp alongside FindPathInContainer().
|
||||
*/
|
||||
static bool GetFullObjectName(const Object &obj, StreamString &fullPath);
|
||||
|
||||
protected:
|
||||
/** Pointer to the single active debug-service instance. */
|
||||
static DebugServiceI *instance;
|
||||
};
|
||||
|
||||
} // namespace MARTe
|
||||
|
||||
#endif // DEBUGSERVICEI_H
|
||||
Reference in New Issue
Block a user