169 lines
3.7 KiB
Go
169 lines
3.7 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/evcc-io/evcc/util"
|
|
)
|
|
|
|
const (
|
|
// Time allowed to write a message to the peer
|
|
socketWriteTimeout = 10 * time.Second
|
|
)
|
|
|
|
// socketSubscriber is a middleman between the websocket connection and the hub.
|
|
type socketSubscriber struct {
|
|
send chan []byte
|
|
closeSlow func()
|
|
}
|
|
|
|
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
return c.Write(ctx, websocket.MessageText, msg)
|
|
}
|
|
|
|
// SocketHub maintains the set of active clients and broadcasts messages to the
|
|
// clients.
|
|
type SocketHub struct {
|
|
mu sync.RWMutex
|
|
register chan *socketSubscriber
|
|
subscribers map[*socketSubscriber]struct{}
|
|
}
|
|
|
|
// NewSocketHub creates a web socket hub that distributes meter status and
|
|
// query results for the ui or other clients
|
|
func NewSocketHub() *SocketHub {
|
|
return &SocketHub{
|
|
register: make(chan *socketSubscriber, 1),
|
|
subscribers: make(map[*socketSubscriber]struct{}),
|
|
}
|
|
}
|
|
|
|
// ServeWebsocket handles websocket requests from the peer.
|
|
func (h *SocketHub) ServeWebsocket(w http.ResponseWriter, r *http.Request) {
|
|
acceptOptions := &websocket.AcceptOptions{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
|
|
// https://github.com/nhooyr/websocket/issues/218
|
|
ua := strings.ToLower(r.Header.Get("User-Agent"))
|
|
if strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome") && !strings.Contains(ua, "android") {
|
|
acceptOptions.CompressionMode = websocket.CompressionDisabled
|
|
}
|
|
|
|
conn, err := websocket.Accept(w, r, acceptOptions)
|
|
if err != nil {
|
|
log.ERROR.Println(err)
|
|
return
|
|
}
|
|
defer conn.Close(websocket.StatusInternalError, "")
|
|
|
|
err = h.subscribe(r.Context(), conn)
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
return
|
|
}
|
|
if cs := websocket.CloseStatus(err); cs == websocket.StatusNormalClosure || cs == websocket.StatusGoingAway {
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.ERROR.Println(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *SocketHub) subscribe(ctx context.Context, conn *websocket.Conn) error {
|
|
ctx = conn.CloseRead(ctx)
|
|
|
|
s := &socketSubscriber{
|
|
send: make(chan []byte, 1024),
|
|
closeSlow: func() {
|
|
conn.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
|
|
},
|
|
}
|
|
|
|
h.addSubscriber(s)
|
|
defer h.deleteSubscriber(s)
|
|
|
|
// send welcome message
|
|
h.register <- s
|
|
|
|
for {
|
|
select {
|
|
case msg := <-s.send:
|
|
if err := writeTimeout(ctx, socketWriteTimeout, conn, msg); err != nil {
|
|
return err
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// addSubscriber registers a subscriber.
|
|
func (h *SocketHub) addSubscriber(s *socketSubscriber) {
|
|
h.mu.Lock()
|
|
h.subscribers[s] = struct{}{}
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
// deleteSubscriber deletes the given subscriber.
|
|
func (h *SocketHub) deleteSubscriber(s *socketSubscriber) {
|
|
h.mu.Lock()
|
|
delete(h.subscribers, s)
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *SocketHub) welcome(subscriber *socketSubscriber, params []util.Param) {
|
|
var msg strings.Builder
|
|
msg.WriteString("{")
|
|
for _, p := range params {
|
|
if msg.Len() > 1 {
|
|
msg.WriteString(",")
|
|
}
|
|
msg.WriteString(kv(p))
|
|
}
|
|
msg.WriteString("}")
|
|
|
|
// should not block
|
|
subscriber.send <- []byte(msg.String())
|
|
}
|
|
|
|
func (h *SocketHub) broadcast(p util.Param) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
if len(h.subscribers) > 0 {
|
|
msg := "{" + kv(p) + "}"
|
|
|
|
for s := range h.subscribers {
|
|
select {
|
|
case s.send <- []byte(msg):
|
|
default:
|
|
s.closeSlow()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run starts data and status distribution
|
|
func (h *SocketHub) Run(in <-chan util.Param, cache *util.Cache) {
|
|
for {
|
|
select {
|
|
case client := <-h.register:
|
|
h.welcome(client, cache.All())
|
|
case msg, ok := <-in:
|
|
if !ok {
|
|
return // break if channel closed
|
|
}
|
|
h.broadcast(msg)
|
|
}
|
|
}
|
|
}
|