290 lines
8.2 KiB
Go
290 lines
8.2 KiB
Go
package events
|
|
|
|
import (
|
|
"os"
|
|
"seanime/internal/util"
|
|
"seanime/internal/util/result"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type WSEventManagerInterface interface {
|
|
SendEvent(t string, payload interface{})
|
|
SendEventTo(clientId string, t string, payload interface{}, noLog ...bool)
|
|
SubscribeToClientEvents(id string) *ClientEventSubscriber
|
|
SubscribeToClientNativePlayerEvents(id string) *ClientEventSubscriber
|
|
SubscribeToClientNakamaEvents(id string) *ClientEventSubscriber
|
|
UnsubscribeFromClientEvents(id string)
|
|
}
|
|
|
|
type GlobalWSEventManagerWrapper struct {
|
|
WSEventManager WSEventManagerInterface
|
|
}
|
|
|
|
var GlobalWSEventManager *GlobalWSEventManagerWrapper
|
|
|
|
func (w *GlobalWSEventManagerWrapper) SendEvent(t string, payload interface{}) {
|
|
if w.WSEventManager == nil {
|
|
return
|
|
}
|
|
w.WSEventManager.SendEvent(t, payload)
|
|
}
|
|
|
|
func (w *GlobalWSEventManagerWrapper) SendEventTo(clientId string, t string, payload interface{}, noLog ...bool) {
|
|
if w.WSEventManager == nil {
|
|
return
|
|
}
|
|
w.WSEventManager.SendEventTo(clientId, t, payload, noLog...)
|
|
}
|
|
|
|
type (
|
|
// WSEventManager holds the websocket connection instance.
|
|
// It is attached to the App instance, so it is available to other handlers.
|
|
WSEventManager struct {
|
|
Conns []*WSConn
|
|
Logger *zerolog.Logger
|
|
hasHadConnection bool
|
|
mu sync.Mutex
|
|
eventMu sync.RWMutex
|
|
clientEventSubscribers *result.Map[string, *ClientEventSubscriber]
|
|
clientNativePlayerEventSubscribers *result.Map[string, *ClientEventSubscriber]
|
|
nakamaEventSubscribers *result.Map[string, *ClientEventSubscriber]
|
|
}
|
|
|
|
ClientEventSubscriber struct {
|
|
Channel chan *WebsocketClientEvent
|
|
mu sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
WSConn struct {
|
|
ID string
|
|
Conn *websocket.Conn
|
|
}
|
|
|
|
WSEvent struct {
|
|
Type string `json:"type"`
|
|
Payload interface{} `json:"payload"`
|
|
}
|
|
)
|
|
|
|
// NewWSEventManager creates a new WSEventManager instance for App.
|
|
func NewWSEventManager(logger *zerolog.Logger) *WSEventManager {
|
|
ret := &WSEventManager{
|
|
Logger: logger,
|
|
Conns: make([]*WSConn, 0),
|
|
clientEventSubscribers: result.NewResultMap[string, *ClientEventSubscriber](),
|
|
clientNativePlayerEventSubscribers: result.NewResultMap[string, *ClientEventSubscriber](),
|
|
nakamaEventSubscribers: result.NewResultMap[string, *ClientEventSubscriber](),
|
|
}
|
|
GlobalWSEventManager = &GlobalWSEventManagerWrapper{
|
|
WSEventManager: ret,
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// ExitIfNoConnsAsDesktopSidecar monitors the websocket connection as a desktop sidecar.
|
|
// It checks for a connection every 5 seconds. If a connection is lost, it starts a countdown a waits for 15 seconds.
|
|
// If a connection is not established within 15 seconds, it will exit the app.
|
|
func (m *WSEventManager) ExitIfNoConnsAsDesktopSidecar() {
|
|
go func() {
|
|
defer util.HandlePanicInModuleThen("events/ExitIfNoConnsAsDesktopSidecar", func() {})
|
|
|
|
m.Logger.Info().Msg("ws: Monitoring connection as desktop sidecar")
|
|
// Create a ticker to check connection every 5 seconds
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
// Track connection loss time
|
|
var connectionLostTime time.Time
|
|
exitTimeout := 10 * time.Second
|
|
|
|
for range ticker.C {
|
|
// Check WebSocket connection status
|
|
if len(m.Conns) == 0 && m.hasHadConnection {
|
|
// If not connected and first detection of connection loss
|
|
if connectionLostTime.IsZero() {
|
|
m.Logger.Warn().Msg("ws: No connection detected. Starting countdown...")
|
|
connectionLostTime = time.Now()
|
|
}
|
|
|
|
// Check if connection has been lost for more than 15 seconds
|
|
if time.Since(connectionLostTime) > exitTimeout {
|
|
m.Logger.Warn().Msg("ws: No connection detected for 10 seconds. Exiting...")
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
// Connection is active, reset connection lost time
|
|
connectionLostTime = time.Time{}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (m *WSEventManager) AddConn(id string, conn *websocket.Conn) {
|
|
m.hasHadConnection = true
|
|
m.Conns = append(m.Conns, &WSConn{
|
|
ID: id,
|
|
Conn: conn,
|
|
})
|
|
}
|
|
|
|
func (m *WSEventManager) RemoveConn(id string) {
|
|
for i, conn := range m.Conns {
|
|
if conn.ID == id {
|
|
m.Conns = append(m.Conns[:i], m.Conns[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// SendEvent sends a websocket event to the client.
|
|
func (m *WSEventManager) SendEvent(t string, payload interface{}) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
// If there's no connection, do nothing
|
|
//if m.Conn == nil {
|
|
// return
|
|
//}
|
|
|
|
if t != PlaybackManagerProgressPlaybackState && payload == nil {
|
|
m.Logger.Trace().Str("type", t).Msg("ws: Sending message")
|
|
}
|
|
|
|
for _, conn := range m.Conns {
|
|
err := conn.Conn.WriteJSON(WSEvent{
|
|
Type: t,
|
|
Payload: payload,
|
|
})
|
|
if err != nil {
|
|
// Note: NaN error coming from [progress_tracking.go]
|
|
//m.Logger.Err(err).Msg("ws: Failed to send message")
|
|
}
|
|
//m.Logger.Trace().Str("type", t).Msg("ws: Sent message")
|
|
}
|
|
|
|
//err := m.Conn.WriteJSON(WSEvent{
|
|
// Type: t,
|
|
// Payload: payload,
|
|
//})
|
|
//if err != nil {
|
|
// m.Logger.Err(err).Msg("ws: Failed to send message")
|
|
//}
|
|
//m.Logger.Trace().Str("type", t).Msg("ws: Sent message")
|
|
}
|
|
|
|
// SendEventTo sends a websocket event to the specified client.
|
|
func (m *WSEventManager) SendEventTo(clientId string, t string, payload interface{}, noLog ...bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for _, conn := range m.Conns {
|
|
if conn.ID == clientId {
|
|
if t != "pong" {
|
|
if len(noLog) == 0 || !noLog[0] {
|
|
truncated := spew.Sprint(payload)
|
|
if len(truncated) > 500 {
|
|
truncated = truncated[:500] + "..."
|
|
}
|
|
m.Logger.Trace().Str("to", clientId).Str("type", t).Str("payload", truncated).Msg("ws: Sending message")
|
|
}
|
|
}
|
|
_ = conn.Conn.WriteJSON(WSEvent{
|
|
Type: t,
|
|
Payload: payload,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *WSEventManager) SendStringTo(clientId string, s string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for _, conn := range m.Conns {
|
|
if conn.ID == clientId {
|
|
_ = conn.Conn.WriteMessage(websocket.TextMessage, []byte(s))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *WSEventManager) OnClientEvent(event *WebsocketClientEvent) {
|
|
m.eventMu.RLock()
|
|
defer m.eventMu.RUnlock()
|
|
|
|
onEvent := func(key string, subscriber *ClientEventSubscriber) bool {
|
|
go func() {
|
|
defer util.HandlePanicInModuleThen("events/OnClientEvent/clientNativePlayerEventSubscribers", func() {})
|
|
subscriber.mu.RLock()
|
|
defer subscriber.mu.RUnlock()
|
|
if !subscriber.closed {
|
|
select {
|
|
case subscriber.Channel <- event:
|
|
default:
|
|
// Channel is blocked, skip sending
|
|
m.Logger.Warn().Msg("ws: Client event channel is blocked, event dropped")
|
|
}
|
|
}
|
|
}()
|
|
return true
|
|
}
|
|
|
|
switch event.Type {
|
|
case NativePlayerEventType:
|
|
m.clientNativePlayerEventSubscribers.Range(onEvent)
|
|
case NakamaEventType:
|
|
m.nakamaEventSubscribers.Range(onEvent)
|
|
default:
|
|
m.clientEventSubscribers.Range(onEvent)
|
|
}
|
|
}
|
|
|
|
func (m *WSEventManager) SubscribeToClientEvents(id string) *ClientEventSubscriber {
|
|
subscriber := &ClientEventSubscriber{
|
|
Channel: make(chan *WebsocketClientEvent, 900),
|
|
}
|
|
m.clientEventSubscribers.Set(id, subscriber)
|
|
return subscriber
|
|
}
|
|
|
|
func (m *WSEventManager) SubscribeToClientNativePlayerEvents(id string) *ClientEventSubscriber {
|
|
subscriber := &ClientEventSubscriber{
|
|
Channel: make(chan *WebsocketClientEvent, 100),
|
|
}
|
|
m.clientNativePlayerEventSubscribers.Set(id, subscriber)
|
|
return subscriber
|
|
}
|
|
|
|
func (m *WSEventManager) SubscribeToClientNakamaEvents(id string) *ClientEventSubscriber {
|
|
subscriber := &ClientEventSubscriber{
|
|
Channel: make(chan *WebsocketClientEvent, 100),
|
|
}
|
|
m.nakamaEventSubscribers.Set(id, subscriber)
|
|
return subscriber
|
|
}
|
|
|
|
func (m *WSEventManager) UnsubscribeFromClientEvents(id string) {
|
|
m.eventMu.Lock()
|
|
defer m.eventMu.Unlock()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
m.Logger.Warn().Msg("ws: Failed to unsubscribe from client events")
|
|
}
|
|
}()
|
|
subscriber, ok := m.clientEventSubscribers.Get(id)
|
|
if !ok {
|
|
subscriber, ok = m.clientNativePlayerEventSubscribers.Get(id)
|
|
}
|
|
if ok {
|
|
subscriber.mu.Lock()
|
|
defer subscriber.mu.Unlock()
|
|
subscriber.closed = true
|
|
m.clientEventSubscribers.Delete(id)
|
|
close(subscriber.Channel)
|
|
}
|
|
}
|