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:
@@ -81,7 +81,8 @@ func broadcastHub(hub *wshub.Hub, v any) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type MarteController struct {
|
type MarteController struct {
|
||||||
hub *wshub.Hub
|
hub *wshub.Hub
|
||||||
|
sink func(v any)
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
tcpConn net.Conn
|
tcpConn net.Conn
|
||||||
@@ -135,6 +136,7 @@ func NewMarteController(hub *wshub.Hub) *MarteController {
|
|||||||
forcedState: make(map[string]string),
|
forcedState: make(map[string]string),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
mc.sink = func(v any) { broadcastHub(mc.hub, v) }
|
||||||
// Register the new-client hook so connection + forced/traced state is
|
// Register the new-client hook so connection + forced/traced state is
|
||||||
// replayed to any browser that connects (or reconnects) while the server
|
// replayed to any browser that connects (or reconnects) while the server
|
||||||
// already holds a live MARTe2 TCP session.
|
// already holds a live MARTe2 TCP session.
|
||||||
@@ -142,6 +144,20 @@ func NewMarteController(hub *wshub.Hub) *MarteController {
|
|||||||
return mc
|
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 {
|
func (m *MarteController) IsConnected() bool {
|
||||||
return atomic.LoadInt32(&m.connected) == 1
|
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.stopCh = make(chan struct{})
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
// Update source state so the browser shows "connecting".
|
// Update source state so the browser shows "connecting". No-op headless
|
||||||
m.hub.SetSourceState("debug", "connecting")
|
// (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"),
|
"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),
|
"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.baseTsSet = false
|
||||||
m.basesMu.Unlock()
|
m.basesMu.Unlock()
|
||||||
m.discoverAcc = nil
|
m.discoverAcc = nil
|
||||||
m.hub.SetSourceState("debug", "disconnected")
|
if m.hub != nil {
|
||||||
|
m.hub.SetSourceState("debug", "disconnected")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MarteController) stopped() bool {
|
func (m *MarteController) stopped() bool {
|
||||||
@@ -299,7 +320,7 @@ func (m *MarteController) HandleBrowserCommand(msg []byte) {
|
|||||||
// forwarded to the MARTe2 TCP control connection.
|
// forwarded to the MARTe2 TCP control connection.
|
||||||
if isDangerousCommand(cmd) {
|
if isDangerousCommand(cmd) {
|
||||||
if !dangerousCommandsEnabled {
|
if !dangerousCommandsEnabled {
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
||||||
"level": "WARNING",
|
"level": "WARNING",
|
||||||
"message": fmt.Sprintf("Blocked dangerous command (requires --enable-dangerous-commands): %s", cmd),
|
"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() {
|
for !m.stopped() {
|
||||||
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
||||||
"level": "WARNING", "message": fmt.Sprintf("TCP %s: %v — retrying…", addr, err),
|
"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()
|
m.mu.Unlock()
|
||||||
|
|
||||||
atomic.StoreInt32(&m.connected, 1)
|
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
|
// Send SERVICE_INFO to auto-discover ports
|
||||||
m.writeCmd("SERVICE_INFO")
|
m.writeCmd("SERVICE_INFO")
|
||||||
@@ -346,7 +367,7 @@ func (m *MarteController) runTCP(host string, port int) {
|
|||||||
m.readLoop(conn)
|
m.readLoop(conn)
|
||||||
|
|
||||||
atomic.StoreInt32(&m.connected, 0)
|
atomic.StoreInt32(&m.connected, 0)
|
||||||
broadcastHub(m.hub, map[string]any{"type": "disconnected"})
|
m.sink(map[string]any{"type": "disconnected"})
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.tcpConn = nil
|
m.tcpConn = nil
|
||||||
@@ -372,7 +393,7 @@ func (m *MarteController) writeCmd(cmd string) {
|
|||||||
silent := cmd == "STEP_STATUS" || cmd == "INFO"
|
silent := cmd == "STEP_STATUS" || cmd == "INFO"
|
||||||
if !silent {
|
if !silent {
|
||||||
log.Printf("[→MARTe] %s", cmd)
|
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"),
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
||||||
"level": "CMD", "message": fmt.Sprintf("→ %s", cmd),
|
"level": "CMD", "message": fmt.Sprintf("→ %s", cmd),
|
||||||
})
|
})
|
||||||
@@ -514,7 +535,7 @@ func (m *MarteController) handleJSONResponse(tag, data string) {
|
|||||||
silent := tag == "STEP_STATUS" || tag == "INFO"
|
silent := tag == "STEP_STATUS" || tag == "INFO"
|
||||||
if !silent {
|
if !silent {
|
||||||
log.Printf("[←MARTe] %s %d bytes", tag, len(data))
|
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"),
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
||||||
"level": "RESP", "message": fmt.Sprintf("← %s (%d B)", tag, len(data)),
|
"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
|
raw := m.rawSigs
|
||||||
m.rawSigsMu.RUnlock()
|
m.rawSigsMu.RUnlock()
|
||||||
if len(raw) > 0 {
|
if len(raw) > 0 {
|
||||||
m.hub.UpdateConfigForSource("debug", m.translateSignalNames(raw))
|
if m.hub != nil {
|
||||||
|
m.hub.UpdateConfigForSource("debug", m.translateSignalNames(raw))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
m.synthesizeHubConfig(all)
|
m.synthesizeHubConfig(all)
|
||||||
}
|
}
|
||||||
// Re-marshal the merged list so the browser gets a single consistent blob.
|
// Re-marshal the merged list so the browser gets a single consistent blob.
|
||||||
merged, _ := json.Marshal(discoverResp{Signals: all})
|
merged, _ := json.Marshal(discoverResp{Signals: all})
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "response", "tag": "DISCOVER", "data": string(merged),
|
"type": "response", "tag": "DISCOVER", "data": string(merged),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
case "TREE":
|
case "TREE":
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "tree_node",
|
"type": "tree_node",
|
||||||
"data": data,
|
"data": data,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "response",
|
"type": "response",
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"data": data,
|
"data": data,
|
||||||
@@ -586,13 +609,13 @@ func (m *MarteController) handleTextLine(line string) {
|
|||||||
fmt.Sscanf(p[8:], "%d", &newLog)
|
fmt.Sscanf(p[8:], "%d", &newLog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "response",
|
"type": "response",
|
||||||
"tag": "SERVICE_INFO",
|
"tag": "SERVICE_INFO",
|
||||||
"data": line[len("OK SERVICE_INFO "):],
|
"data": line[len("OK SERVICE_INFO "):],
|
||||||
})
|
})
|
||||||
if newUDP > 0 || newLog > 0 {
|
if newUDP > 0 || newLog > 0 {
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "service_config",
|
"type": "service_config",
|
||||||
"udp_port": newUDP,
|
"udp_port": newUDP,
|
||||||
"log_port": newLog,
|
"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",
|
"type": "text_line",
|
||||||
"data": 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
|
// buffer and limiting live streaming to the fraction of a second that
|
||||||
// accumulated before the DISCOVER response arrived.
|
// accumulated before the DISCOVER response arrived.
|
||||||
translated := m.translateSignalNames(sigInfos)
|
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 {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("UDP bind on %s failed: %v — rebuild DebugService C++ and restart", addr, err)
|
msg := fmt.Sprintf("UDP bind on %s failed: %v — rebuild DebugService C++ and restart", addr, err)
|
||||||
log.Printf("[debug-udp] %s", msg)
|
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"),
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
||||||
"level": "ERROR", "message": msg,
|
"level": "ERROR", "message": msg,
|
||||||
})
|
})
|
||||||
@@ -861,7 +886,7 @@ func (m *MarteController) runDebugUDP(host string, port int) {
|
|||||||
conn.SetReadBuffer(10 * 1024 * 1024)
|
conn.SetReadBuffer(10 * 1024 * 1024)
|
||||||
|
|
||||||
log.Printf("[debug-udp] listening on %s for UDPS packets", addr)
|
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"),
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
||||||
"level": "INFO", "message": fmt.Sprintf("UDP listener bound on %s", addr),
|
"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)
|
sigs = m.translateSignalNames(sigs)
|
||||||
currentSigs = sigs
|
currentSigs = sigs
|
||||||
currentPublishMode = pm
|
currentPublishMode = pm
|
||||||
m.hub.UpdateConfigForSource("debug", sigs)
|
if m.hub != nil {
|
||||||
m.hub.SetSourceState("debug", "connected")
|
m.hub.UpdateConfigForSource("debug", sigs)
|
||||||
|
m.hub.SetSourceState("debug", "connected")
|
||||||
|
}
|
||||||
|
|
||||||
case udpsprotocol.PktData:
|
case udpsprotocol.PktData:
|
||||||
if len(currentSigs) == 0 {
|
if len(currentSigs) == 0 {
|
||||||
@@ -937,8 +964,10 @@ func (m *MarteController) runDebugUDP(host string, port int) {
|
|||||||
log.Printf("[debug-udp] parse data: %v", err)
|
log.Printf("[debug-udp] parse data: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, s := range samples {
|
if m.hub != nil {
|
||||||
m.hub.PushDataForSource("debug", s)
|
for _, s := range samples {
|
||||||
|
m.hub.PushDataForSource("debug", s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -972,7 +1001,7 @@ func (m *MarteController) runLog(host string, port int) {
|
|||||||
}
|
}
|
||||||
level := rest[:idx]
|
level := rest[:idx]
|
||||||
msg := rest[idx+1:]
|
msg := rest[idx+1:]
|
||||||
broadcastHub(m.hub, map[string]any{
|
m.sink(map[string]any{
|
||||||
"type": "log",
|
"type": "log",
|
||||||
"time": time.Now().Format("15:04:05.000"),
|
"time": time.Now().Format("15:04:05.000"),
|
||||||
"level": level,
|
"level": level,
|
||||||
|
|||||||
Reference in New Issue
Block a user