refactor(debugger): extract MarteController sink so it can run headless
Add a sink func(v any) field so MarteController's event stream can be routed somewhere other than the browser WebSocket hub. NewMarteController now sets sink to broadcast through the hub as before; a new NewHeadlessMarteController(sink) constructor builds an instance with hub == nil for the upcoming debugclient E2E tool. Direct m.hub.* calls (SetSourceState/UpdateConfigForSource/PushDataForSource) are now guarded with nil checks so a headless controller doesn't panic.
This commit is contained in:
@@ -82,6 +82,7 @@ func broadcastHub(hub *wshub.Hub, v any) {
|
||||
|
||||
type MarteController struct {
|
||||
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".
|
||||
// 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,8 +251,10 @@ func (m *MarteController) Disconnect() {
|
||||
m.baseTsSet = false
|
||||
m.basesMu.Unlock()
|
||||
m.discoverAcc = nil
|
||||
if m.hub != nil {
|
||||
m.hub.SetSourceState("debug", "disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MarteController) stopped() bool {
|
||||
select {
|
||||
@@ -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 {
|
||||
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,8 +846,10 @@ 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)
|
||||
if m.hub != nil {
|
||||
m.hub.UpdateConfigForSource("debug", translated)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Debug UDP receiver — receives UDPS packets from DebugService
|
||||
@@ -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
|
||||
if m.hub != nil {
|
||||
m.hub.UpdateConfigForSource("debug", sigs)
|
||||
m.hub.SetSourceState("debug", "connected")
|
||||
}
|
||||
|
||||
case udpsprotocol.PktData:
|
||||
if len(currentSigs) == 0 {
|
||||
@@ -937,11 +964,13 @@ func (m *MarteController) runDebugUDP(host string, port int) {
|
||||
log.Printf("[debug-udp] parse data: %v", err)
|
||||
continue
|
||||
}
|
||||
if m.hub != nil {
|
||||
for _, s := range samples {
|
||||
m.hub.PushDataForSource("debug", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[debug-udp] stopped")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user