Files
marte-debug/Tools/go_web_client/main.go
T
2026-05-20 17:21:33 +02:00

246 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"embed"
"encoding/json"
"flag"
"io/fs"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
// buildVersion is injected at link time:
//
// go build -ldflags "-X main.buildVersion=$(git rev-parse --short HEAD)"
var buildVersion = "dev"
//go:embed static
var staticFiles embed.FS
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// ---------------------------------------------------------------------------
// Wire message types
// ---------------------------------------------------------------------------
type InMsg struct {
Type string `json:"type"`
Data json.RawMessage `json:"data,omitempty"`
}
type ConnectData struct {
Host string `json:"host"`
Port int `json:"port"`
UDPPort int `json:"udp_port"`
LogPort int `json:"log_port"`
}
type CmdData struct {
Cmd string `json:"cmd"`
}
// ---------------------------------------------------------------------------
// Hub fan-out WS messages to all connected browsers
// ---------------------------------------------------------------------------
type Hub struct {
mu sync.RWMutex
clients map[*WSClient]struct{}
marte *MarteClient
}
func newHub() *Hub {
return &Hub{clients: make(map[*WSClient]struct{})}
}
func (h *Hub) add(c *WSClient) {
h.mu.Lock()
h.clients[c] = struct{}{}
h.mu.Unlock()
}
func (h *Hub) remove(c *WSClient) {
h.mu.Lock()
delete(h.clients, c)
h.mu.Unlock()
}
func (h *Hub) broadcast(msg []byte) {
h.mu.RLock()
defer h.mu.RUnlock()
for c := range h.clients {
select {
case c.send <- msg:
default:
}
}
}
// ---------------------------------------------------------------------------
// WSClient one browser tab
// ---------------------------------------------------------------------------
type WSClient struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
func (c *WSClient) writePump() {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case msg, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func (c *WSClient) readPump() {
defer func() {
c.hub.remove(c)
c.conn.Close()
}()
c.conn.SetReadLimit(256 * 1024)
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, raw, err := c.conn.ReadMessage()
if err != nil {
break
}
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
var in InMsg
if err := json.Unmarshal(raw, &in); err != nil {
continue
}
c.handleIncoming(in)
}
}
func (c *WSClient) handleIncoming(in InMsg) {
m := c.hub.marte
switch in.Type {
case "connect":
var d ConnectData
if err := json.Unmarshal(in.Data, &d); err != nil {
return
}
if d.Host == "" {
d.Host = "127.0.0.1"
}
if d.Port == 0 {
d.Port = 8080
}
if d.UDPPort == 0 {
d.UDPPort = 8081
}
if d.LogPort == 0 {
d.LogPort = 8082
}
log.Printf("[WS] connect request host=%s cmd=%d udp=%d log=%d",
d.Host, d.Port, d.UDPPort, d.LogPort)
m.Connect(d.Host, d.Port, d.UDPPort, d.LogPort)
case "disconnect":
log.Printf("[WS] disconnect request")
m.Disconnect()
case "cmd":
var d CmdData
if err := json.Unmarshal(in.Data, &d); err != nil {
return
}
m.SendCommand(d.Cmd)
}
}
// ---------------------------------------------------------------------------
// HTTP handler
// ---------------------------------------------------------------------------
func wsHandler(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
c := &WSClient{hub: hub, conn: conn, send: make(chan []byte, 256)}
hub.add(c)
// Send current connection status to newly joined client.
if hub.marte.IsConnected() {
b, _ := json.Marshal(map[string]any{"type": "connected"})
c.send <- b
// Also push current signal list if already discovered.
hub.marte.SendCachedState(c)
}
go c.writePump()
c.readPump() // blocks until client disconnects
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
func main() {
addr := flag.String("addr", ":7777", "HTTP listen address")
flag.Parse()
hub := newHub()
hub.marte = newMarteClient(hub)
sub, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatal(err)
}
fileServer := http.FileServer(http.FS(sub))
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
wsHandler(hub, w, r)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Serve index.html for the root path explicitly.
if r.URL.Path == "/" {
data, err := staticFiles.ReadFile("static/index.html")
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(data)
return
}
fileServer.ServeHTTP(w, r)
})
log.Printf("MARTe2 Web Debug Client build=%s listening on %s", buildVersion, *addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}