diff --git a/Client/debugger/martecontrol.go b/Client/debugger/martecontrol.go index 8fed345..1896cda 100644 --- a/Client/debugger/martecontrol.go +++ b/Client/debugger/martecontrol.go @@ -81,7 +81,8 @@ func broadcastHub(hub *wshub.Hub, v any) { // --------------------------------------------------------------------------- type MarteController struct { - hub *wshub.Hub + hub *wshub.Hub + sink func(v any) mu sync.Mutex tcpConn net.Conn @@ -135,6 +136,7 @@ func NewMarteController(hub *wshub.Hub) *MarteController { forcedState: make(map[string]string), stopCh: make(chan struct{}), } + mc.sink = func(v any) { broadcastHub(mc.hub, v) } // Register the new-client hook so connection + forced/traced state is // replayed to any browser that connects (or reconnects) while the server // already holds a live MARTe2 TCP session. @@ -142,6 +144,20 @@ func NewMarteController(hub *wshub.Hub) *MarteController { return mc } +// NewHeadlessMarteController creates a MarteController with no WebSocket hub, +// routing all events through sink instead (used by the debugclient E2E test tool). +func NewHeadlessMarteController(sink func(v any)) *MarteController { + mc := &MarteController{ + hub: nil, + signals: make(map[uint32]*SignalMeta), + tracedNames: make(map[string]bool), + forcedState: make(map[string]string), + stopCh: make(chan struct{}), + } + mc.sink = sink + return mc +} + func (m *MarteController) IsConnected() bool { return atomic.LoadInt32(&m.connected) == 1 } @@ -200,10 +216,13 @@ func (m *MarteController) Connect(host string, cmdPort, udpPort, logPort int) { m.stopCh = make(chan struct{}) m.mu.Unlock() - // Update source state so the browser shows "connecting". - m.hub.SetSourceState("debug", "connecting") + // Update source state so the browser shows "connecting". No-op headless + // (m.hub == nil for NewHeadlessMarteController instances). + if m.hub != nil { + m.hub.SetSourceState("debug", "connecting") + } - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "INFO", "message": fmt.Sprintf("Connecting to %s cmd=%d udp=%d log=%d", host, cmdPort, udpPort, logPort), }) @@ -232,7 +251,9 @@ func (m *MarteController) Disconnect() { m.baseTsSet = false m.basesMu.Unlock() m.discoverAcc = nil - m.hub.SetSourceState("debug", "disconnected") + if m.hub != nil { + m.hub.SetSourceState("debug", "disconnected") + } } func (m *MarteController) stopped() bool { @@ -299,7 +320,7 @@ func (m *MarteController) HandleBrowserCommand(msg []byte) { // forwarded to the MARTe2 TCP control connection. if isDangerousCommand(cmd) { if !dangerousCommandsEnabled { - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "WARNING", "message": fmt.Sprintf("Blocked dangerous command (requires --enable-dangerous-commands): %s", cmd), @@ -321,7 +342,7 @@ func (m *MarteController) runTCP(host string, port int) { for !m.stopped() { conn, err := net.DialTimeout("tcp", addr, 5*time.Second) if err != nil { - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "WARNING", "message": fmt.Sprintf("TCP %s: %v — retrying…", addr, err), }) @@ -336,7 +357,7 @@ func (m *MarteController) runTCP(host string, port int) { m.mu.Unlock() atomic.StoreInt32(&m.connected, 1) - broadcastHub(m.hub, map[string]any{"type": "connected"}) + m.sink(map[string]any{"type": "connected"}) // Send SERVICE_INFO to auto-discover ports m.writeCmd("SERVICE_INFO") @@ -346,7 +367,7 @@ func (m *MarteController) runTCP(host string, port int) { m.readLoop(conn) atomic.StoreInt32(&m.connected, 0) - broadcastHub(m.hub, map[string]any{"type": "disconnected"}) + m.sink(map[string]any{"type": "disconnected"}) m.mu.Lock() m.tcpConn = nil @@ -372,7 +393,7 @@ func (m *MarteController) writeCmd(cmd string) { silent := cmd == "STEP_STATUS" || cmd == "INFO" if !silent { log.Printf("[→MARTe] %s", cmd) - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "CMD", "message": fmt.Sprintf("→ %s", cmd), }) @@ -514,7 +535,7 @@ func (m *MarteController) handleJSONResponse(tag, data string) { silent := tag == "STEP_STATUS" || tag == "INFO" if !silent { log.Printf("[←MARTe] %s %d bytes", tag, len(data)) - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "RESP", "message": fmt.Sprintf("← %s (%d B)", tag, len(data)), }) @@ -549,25 +570,27 @@ func (m *MarteController) handleJSONResponse(tag, data string) { raw := m.rawSigs m.rawSigsMu.RUnlock() if len(raw) > 0 { - m.hub.UpdateConfigForSource("debug", m.translateSignalNames(raw)) + if m.hub != nil { + m.hub.UpdateConfigForSource("debug", m.translateSignalNames(raw)) + } } else { m.synthesizeHubConfig(all) } // Re-marshal the merged list so the browser gets a single consistent blob. merged, _ := json.Marshal(discoverResp{Signals: all}) - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "response", "tag": "DISCOVER", "data": string(merged), }) return case "TREE": - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "tree_node", "data": data, }) return } - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "response", "tag": tag, "data": data, @@ -586,13 +609,13 @@ func (m *MarteController) handleTextLine(line string) { fmt.Sscanf(p[8:], "%d", &newLog) } } - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "response", "tag": "SERVICE_INFO", "data": line[len("OK SERVICE_INFO "):], }) if newUDP > 0 || newLog > 0 { - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "service_config", "udp_port": newUDP, "log_port": newLog, @@ -616,7 +639,7 @@ func (m *MarteController) handleTextLine(line string) { } } } - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "text_line", "data": line, }) @@ -823,7 +846,9 @@ func (m *MarteController) synthesizeHubConfig(sigs []discoverSignalJSON) { // buffer and limiting live streaming to the fraction of a second that // accumulated before the DISCOVER response arrived. translated := m.translateSignalNames(sigInfos) - m.hub.UpdateConfigForSource("debug", translated) + if m.hub != nil { + m.hub.UpdateConfigForSource("debug", translated) + } } // --------------------------------------------------------------------------- @@ -850,7 +875,7 @@ func (m *MarteController) runDebugUDP(host string, port int) { if err != nil { msg := fmt.Sprintf("UDP bind on %s failed: %v — rebuild DebugService C++ and restart", addr, err) log.Printf("[debug-udp] %s", msg) - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "ERROR", "message": msg, }) @@ -861,7 +886,7 @@ func (m *MarteController) runDebugUDP(host string, port int) { conn.SetReadBuffer(10 * 1024 * 1024) log.Printf("[debug-udp] listening on %s for UDPS packets", addr) - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": "INFO", "message": fmt.Sprintf("UDP listener bound on %s", addr), }) @@ -914,8 +939,10 @@ func (m *MarteController) runDebugUDP(host string, port int) { sigs = m.translateSignalNames(sigs) currentSigs = sigs currentPublishMode = pm - m.hub.UpdateConfigForSource("debug", sigs) - m.hub.SetSourceState("debug", "connected") + if m.hub != nil { + m.hub.UpdateConfigForSource("debug", sigs) + m.hub.SetSourceState("debug", "connected") + } case udpsprotocol.PktData: if len(currentSigs) == 0 { @@ -937,8 +964,10 @@ func (m *MarteController) runDebugUDP(host string, port int) { log.Printf("[debug-udp] parse data: %v", err) continue } - for _, s := range samples { - m.hub.PushDataForSource("debug", s) + if m.hub != nil { + for _, s := range samples { + m.hub.PushDataForSource("debug", s) + } } } } @@ -972,7 +1001,7 @@ func (m *MarteController) runLog(host string, port int) { } level := rest[:idx] msg := rest[idx+1:] - broadcastHub(m.hub, map[string]any{ + m.sink(map[string]any{ "type": "log", "time": time.Now().Format("15:04:05.000"), "level": level,