github.com/kivutar/go-ethereum@v1.7.4-0.20180117074026-6fdb126e9630/dashboard/dashboard.go (about)

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