github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/internal/daemon/handle_sockets.go (about)

     1  // Copyright 2021 The TrueBlocks Authors. All rights reserved.
     2  // Use of this source code is governed by a license that can
     3  // be found in the LICENSE file.
     4  
     5  package daemonPkg
     6  
     7  import (
     8  	"fmt"
     9  	"net"
    10  	"net/http"
    11  
    12  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    13  	"github.com/gorilla/websocket"
    14  )
    15  
    16  // MessageType is a message carried in a string
    17  type MessageType string
    18  
    19  const (
    20  	// CommandErrorMessage is a message send when the server encounters an error
    21  	CommandErrorMessage MessageType = "command_error"
    22  	// CommandOutputMessage is currently not used, but may in the future carry the actual data
    23  	CommandOutputMessage MessageType = "output"
    24  	// ProgressMessage is a message carried on the stderr stream
    25  	ProgressMessage MessageType = "progress"
    26  )
    27  
    28  var upgrader = websocket.Upgrader{}
    29  
    30  // Message is a structure used to send messages via websockets
    31  type Message struct {
    32  	Action  MessageType `json:"action"`
    33  	ID      string      `json:"id"`
    34  	Content string      `json:"content"`
    35  }
    36  
    37  // Connection is a structure representing a websocket connection
    38  type Connection struct {
    39  	connection *websocket.Conn
    40  	pool       *ConnectionPool
    41  	send       chan *Message
    42  }
    43  
    44  // write the message to the connection
    45  func (c *Connection) write() {
    46  	defer func() {
    47  		c.connection.Close()
    48  	}()
    49  
    50  	// this is a common pattern for a websocket connection
    51  	//nolint:staticcheck,gosimple
    52  	for {
    53  		select {
    54  		case message, ok := <-c.send:
    55  			if !ok {
    56  				c.Log("Connection closed")
    57  				_ = c.connection.WriteMessage(websocket.CloseMessage, []byte{})
    58  				return
    59  			}
    60  
    61  			err := c.connection.WriteJSON(message)
    62  			if err != nil {
    63  				// c.Log("Error while sending message, dropping connection: %s", err.Error())
    64  				c.pool.unregister <- c
    65  			}
    66  		}
    67  	}
    68  }
    69  
    70  // RemoteAddr is the other end of the connection
    71  func (c *Connection) RemoteAddr() net.Addr {
    72  	return c.connection.RemoteAddr()
    73  }
    74  
    75  // Log writes a log messages to the server's stderr
    76  func (c *Connection) Log(s string, args ...interface{}) {
    77  	subMsg := fmt.Sprintf(s, args...)
    78  	msg := fmt.Sprintf("%s %s", c.RemoteAddr(), subMsg)
    79  	logger.Info(msg)
    80  }
    81  
    82  // ConnectionPool is the collection of all connections
    83  type ConnectionPool struct {
    84  	connections map[*Connection]bool
    85  	broadcast   chan *Message
    86  	register    chan *Connection
    87  	unregister  chan *Connection
    88  }
    89  
    90  // closeAndDelete cleans up a connection
    91  func closeAndDelete(pool *ConnectionPool, connection *Connection) {
    92  	delete(pool.connections, connection)
    93  	close(connection.send)
    94  }
    95  
    96  // newConnectionPool returns a new connection structure
    97  func newConnectionPool() *ConnectionPool {
    98  	return &ConnectionPool{
    99  		connections: make(map[*Connection]bool),
   100  		broadcast:   make(chan *Message),
   101  		register:    make(chan *Connection),
   102  		unregister:  make(chan *Connection),
   103  	}
   104  }
   105  
   106  // run the web sockets server
   107  func (pool *ConnectionPool) run() {
   108  	// run forever...
   109  	for {
   110  		select {
   111  		// handle a signal to register a new connection
   112  		case connection := <-pool.register:
   113  			connection.Log("Connected (Websockets)")
   114  			pool.connections[connection] = true
   115  		// handle a signal to unregister a connection
   116  		case connection := <-pool.unregister:
   117  			if _, ok := pool.connections[connection]; ok {
   118  				connection.Log("Unregistering connection")
   119  				closeAndDelete(pool, connection)
   120  			}
   121  		// handle a signal to broadcast a message
   122  		case message := <-pool.broadcast:
   123  			for connection := range pool.connections {
   124  				connection.send <- message
   125  			}
   126  		}
   127  	}
   128  }
   129  
   130  // HandleWebsockets handles web sockets
   131  func HandleWebsockets(pool *ConnectionPool, w http.ResponseWriter, r *http.Request) {
   132  	// TODO: the line below allows any connection through WebSockets. Once the server
   133  	// TODO: is ready, we should implement some rules here
   134  	upgrader.CheckOrigin = func(r *http.Request) bool { return true }
   135  
   136  	c, err := upgrader.Upgrade(w, r, nil)
   137  	if err != nil {
   138  		logger.Error("upgrade:", err)
   139  		return
   140  	}
   141  
   142  	connection := &Connection{connection: c, send: make(chan *Message), pool: pool}
   143  	pool.register <- connection
   144  
   145  	go connection.write()
   146  }
   147  
   148  var connectionPool = newConnectionPool()
   149  
   150  // RunWebsocketPool runs the websocket pool
   151  func RunWebsocketPool() {
   152  	go connectionPool.run()
   153  }