github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/dashboard/dashboard.go (about)

     1  // Copyright 2017 The Spectrum Authors
     2  // This file is part of the Spectrum library.
     3  //
     4  // The Spectrum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The Spectrum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the Spectrum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package dashboard
    18  
    19  //go:generate ./assets/node_modules/.bin/webpack --config ./assets/webpack.config.js --context ./assets
    20  //go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/public/...
    21  //go:generate gofmt -s -w assets.go
    22  //go:generate sed -i "s#var _public#//nolint:misspell\\n&#" assets.go
    23  
    24  import (
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net"
    28  	"net/http"
    29  	"path/filepath"
    30  	"sync"
    31  	"sync/atomic"
    32  	"time"
    33  
    34  	"github.com/SmartMeshFoundation/Spectrum/log"
    35  	"github.com/SmartMeshFoundation/Spectrum/p2p"
    36  	"github.com/SmartMeshFoundation/Spectrum/rpc"
    37  	"github.com/rcrowley/go-metrics"
    38  	"golang.org/x/net/websocket"
    39  )
    40  
    41  const (
    42  	memorySampleLimit  = 200 // Maximum number of memory data samples
    43  	trafficSampleLimit = 200 // Maximum number of traffic data samples
    44  )
    45  
    46  var nextID uint32 // Next connection id
    47  
    48  // Dashboard contains the dashboard internals.
    49  type Dashboard struct {
    50  	config *Config
    51  
    52  	listener net.Listener
    53  	conns    map[uint32]*client // Currently live websocket connections
    54  	charts   *HomeMessage
    55  	lock     sync.RWMutex // Lock protecting the dashboard's internals
    56  
    57  	quit chan chan error // Channel used for graceful exit
    58  	wg   sync.WaitGroup
    59  }
    60  
    61  // client represents active websocket connection with a remote browser.
    62  type client struct {
    63  	conn   *websocket.Conn // Particular live websocket connection
    64  	msg    chan Message    // Message queue for the update messages
    65  	logger log.Logger      // Logger for the particular live websocket connection
    66  }
    67  
    68  // New creates a new dashboard instance with the given configuration.
    69  func New(config *Config) (*Dashboard, error) {
    70  	return &Dashboard{
    71  		conns:  make(map[uint32]*client),
    72  		config: config,
    73  		quit:   make(chan chan error),
    74  		charts: &HomeMessage{
    75  			Memory:  &Chart{},
    76  			Traffic: &Chart{},
    77  		},
    78  	}, nil
    79  }
    80  
    81  // Protocols is a meaningless implementation of node.Service.
    82  func (db *Dashboard) Protocols() []p2p.Protocol { return nil }
    83  
    84  // APIs is a meaningless implementation of node.Service.
    85  func (db *Dashboard) APIs() []rpc.API { return nil }
    86  
    87  // Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
    88  func (db *Dashboard) Start(server *p2p.Server) error {
    89  	db.wg.Add(2)
    90  	go db.collectData()
    91  	go db.collectLogs() // In case of removing this line change 2 back to 1 in wg.Add.
    92  
    93  	http.HandleFunc("/", db.webHandler)
    94  	http.Handle("/api", websocket.Handler(db.apiHandler))
    95  
    96  	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port))
    97  	if err != nil {
    98  		return err
    99  	}
   100  	db.listener = listener
   101  
   102  	go http.Serve(listener, nil)
   103  
   104  	return nil
   105  }
   106  
   107  // Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
   108  func (db *Dashboard) Stop() error {
   109  	// Close the connection listener.
   110  	var errs []error
   111  	if err := db.listener.Close(); err != nil {
   112  		errs = append(errs, err)
   113  	}
   114  	// Close the collectors.
   115  	errc := make(chan error, 1)
   116  	for i := 0; i < 2; i++ {
   117  		db.quit <- errc
   118  		if err := <-errc; err != nil {
   119  			errs = append(errs, err)
   120  		}
   121  	}
   122  	// Close the connections.
   123  	db.lock.Lock()
   124  	for _, c := range db.conns {
   125  		if err := c.conn.Close(); err != nil {
   126  			c.logger.Warn("Failed to close connection", "err", err)
   127  		}
   128  	}
   129  	db.lock.Unlock()
   130  
   131  	// Wait until every goroutine terminates.
   132  	db.wg.Wait()
   133  	log.Info("Dashboard stopped")
   134  
   135  	var err error
   136  	if len(errs) > 0 {
   137  		err = fmt.Errorf("%v", errs)
   138  	}
   139  
   140  	return err
   141  }
   142  
   143  // webHandler handles all non-api requests, simply flattening and returning the dashboard website.
   144  func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
   145  	log.Debug("Request", "URL", r.URL)
   146  
   147  	path := r.URL.String()
   148  	if path == "/" {
   149  		path = "/dashboard.html"
   150  	}
   151  	// If the path of the assets is manually set
   152  	if db.config.Assets != "" {
   153  		blob, err := ioutil.ReadFile(filepath.Join(db.config.Assets, path))
   154  		if err != nil {
   155  			log.Warn("Failed to read file", "path", path, "err", err)
   156  			http.Error(w, "not found", http.StatusNotFound)
   157  			return
   158  		}
   159  		w.Write(blob)
   160  		return
   161  	}
   162  	blob, err := Asset(filepath.Join("public", path))
   163  	if err != nil {
   164  		log.Warn("Failed to load the asset", "path", path, "err", err)
   165  		http.Error(w, "not found", http.StatusNotFound)
   166  		return
   167  	}
   168  	w.Write(blob)
   169  }
   170  
   171  // apiHandler handles requests for the dashboard.
   172  func (db *Dashboard) apiHandler(conn *websocket.Conn) {
   173  	id := atomic.AddUint32(&nextID, 1)
   174  	client := &client{
   175  		conn:   conn,
   176  		msg:    make(chan Message, 128),
   177  		logger: log.New("id", id),
   178  	}
   179  	done := make(chan struct{})
   180  
   181  	// Start listening for messages to send.
   182  	db.wg.Add(1)
   183  	go func() {
   184  		defer db.wg.Done()
   185  
   186  		for {
   187  			select {
   188  			case <-done:
   189  				return
   190  			case msg := <-client.msg:
   191  				if err := websocket.JSON.Send(client.conn, msg); err != nil {
   192  					client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
   193  					client.conn.Close()
   194  					return
   195  				}
   196  			}
   197  		}
   198  	}()
   199  	// Send the past data.
   200  	client.msg <- Message{
   201  		Home: &HomeMessage{
   202  			Memory: &Chart{
   203  				History: db.charts.Memory.History,
   204  			},
   205  			Traffic: &Chart{
   206  				History: db.charts.Traffic.History,
   207  			},
   208  		},
   209  	}
   210  	// Start tracking the connection and drop at connection loss.
   211  	db.lock.Lock()
   212  	db.conns[id] = client
   213  	db.lock.Unlock()
   214  	defer func() {
   215  		db.lock.Lock()
   216  		delete(db.conns, id)
   217  		db.lock.Unlock()
   218  	}()
   219  	for {
   220  		fail := []byte{}
   221  		if _, err := conn.Read(fail); err != nil {
   222  			close(done)
   223  			return
   224  		}
   225  		// Ignore all messages
   226  	}
   227  }
   228  
   229  // collectData collects the required data to plot on the dashboard.
   230  func (db *Dashboard) collectData() {
   231  	defer db.wg.Done()
   232  
   233  	for {
   234  		select {
   235  		case errc := <-db.quit:
   236  			errc <- nil
   237  			return
   238  		case <-time.After(db.config.Refresh):
   239  			inboundTraffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
   240  			memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
   241  			now := time.Now()
   242  			memory := &ChartEntry{
   243  				Time:  now,
   244  				Value: memoryInUse,
   245  			}
   246  			traffic := &ChartEntry{
   247  				Time:  now,
   248  				Value: inboundTraffic,
   249  			}
   250  			first := 0
   251  			if len(db.charts.Memory.History) == memorySampleLimit {
   252  				first = 1
   253  			}
   254  			db.charts.Memory.History = append(db.charts.Memory.History[first:], memory)
   255  			first = 0
   256  			if len(db.charts.Traffic.History) == trafficSampleLimit {
   257  				first = 1
   258  			}
   259  			db.charts.Traffic.History = append(db.charts.Traffic.History[first:], traffic)
   260  
   261  			db.sendToAll(&Message{
   262  				Home: &HomeMessage{
   263  					Memory: &Chart{
   264  						New: memory,
   265  					},
   266  					Traffic: &Chart{
   267  						New: traffic,
   268  					},
   269  				},
   270  			})
   271  		}
   272  	}
   273  }
   274  
   275  // collectLogs collects and sends the logs to the active dashboards.
   276  func (db *Dashboard) collectLogs() {
   277  	defer db.wg.Done()
   278  
   279  	id := 1
   280  	// TODO (kurkomisi): log collection comes here.
   281  	for {
   282  		select {
   283  		case errc := <-db.quit:
   284  			errc <- nil
   285  			return
   286  		case <-time.After(db.config.Refresh / 2):
   287  			db.sendToAll(&Message{
   288  				Logs: &LogsMessage{
   289  					Log: fmt.Sprintf("%-4d: This is a fake log.", id),
   290  				},
   291  			})
   292  			id++
   293  		}
   294  	}
   295  }
   296  
   297  // sendToAll sends the given message to the active dashboards.
   298  func (db *Dashboard) sendToAll(msg *Message) {
   299  	db.lock.Lock()
   300  	for _, c := range db.conns {
   301  		select {
   302  		case c.msg <- *msg:
   303  		default:
   304  			c.conn.Close()
   305  		}
   306  	}
   307  	db.lock.Unlock()
   308  }