#ifndef DEBUGSERVICE_H #define DEBUGSERVICE_H #include "BasicTCPSocket.h" #include "BasicUDPSocket.h" #include "ConfigurationDatabase.h" #include "DebugServiceI.h" #include "EmbeddedServiceMethodBinderI.h" #include "MessageI.h" #include "ReferenceContainer.h" #include "ReferenceT.h" #include "SingleThreadService.h" namespace MARTe { class DataSourceI; /** * @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 DebugServiceI { public: friend class DebugServiceTest; CLASS_REGISTER_DECLARATION() DebugService(); virtual ~DebugService(); virtual bool Initialise(StructuredDataI &data); // 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 *activeIndices, Vec *activeSizes, FastPollingMutexSem *activeMutex, volatile bool *anyBreakFlag, Vec *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 *)); // 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 &data); // TCP-transport-specific methods (not part of DebugServiceI) void GetStepStatus(BasicTCPSocket *client); void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *)); void GetSignalValue(const char8 *name, BasicTCPSocket *client); void Discover(BasicTCPSocket *client); void InfoNode(const char8 *path, BasicTCPSocket *client); void ListNodes(const char8 *path, BasicTCPSocket *client); void ServeConfig(BasicTCPSocket *client); void SetFullConfig(ConfigurationDatabase &config); void RebuildConfigFromRegistry(); struct MonitoredSignal { ReferenceT dataSource; uint32 signalIdx; uint32 internalID; uint32 periodMs; uint64 lastPollTime; uint32 size; StreamString path; }; private: void HandleCommand(StreamString cmd, BasicTCPSocket *client); void UpdateBrokersActiveStatus(); void UpdateBrokersBreakStatus(); void InjectTcpLoggerIfNeeded(); uint32 ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix); void PatchRegistry(); void EnrichWithConfig(const char8 *path, StreamString &json); static void JsonifyDatabase(ConfigurationDatabase &db, StreamString &json); ErrorManagement::ErrorType Server(ExecutionInfo &info); ErrorManagement::ErrorType Streamer(ExecutionInfo &info); uint16 controlPort; uint16 streamPort; uint16 logPort; StreamString streamIP; bool isServer; bool suppressTimeoutLogs; volatile bool isPaused; volatile uint32 stepRemaining; StreamString pausedAtGam; StreamString stepThreadFilter; // empty = all threads; non-empty = only this OS thread name BasicTCPSocket tcpServer; BasicUDPSocket udpSocket; class ServiceBinder : public EmbeddedServiceMethodBinderI { public: enum ServiceType { ServerType, StreamerType }; ServiceBinder(DebugService *parent, ServiceType type) : parent(parent), type(type) {} virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info) { if (type == StreamerType) { return parent->Streamer(info); } return parent->Server(info); } private: DebugService *parent; ServiceType type; }; ServiceBinder binderServer; ServiceBinder binderStreamer; SingleThreadService threadService; SingleThreadService streamerService; ThreadIdentifier serverThreadId; ThreadIdentifier streamerThreadId; Vec signals; Vec aliases; Vec brokers; Vec monitoredSignals; 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[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; }; } // namespace MARTe #endif