github.com/openethereum/go-ethereum@v1.9.7/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 yarn --cwd ./assets install
    20  //go:generate yarn --cwd ./assets build
    21  //go:generate yarn --cwd ./assets js-beautify -f bundle.js.map -r -w 1
    22  //go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/index.html assets/bundle.js assets/bundle.js.map
    23  //go:generate sh -c "sed 's#var _bundleJs#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
    24  //go:generate sh -c "sed 's#var _bundleJsMap#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
    25  //go:generate sh -c "sed 's#var _indexHtml#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
    26  //go:generate gofmt -w -s assets.go
    27  
    28  import (
    29  	"fmt"
    30  	"net"
    31  	"net/http"
    32  	"sync"
    33  	"sync/atomic"
    34  	"time"
    35  
    36  	"io"
    37  
    38  	"github.com/ethereum/go-ethereum/log"
    39  	"github.com/ethereum/go-ethereum/p2p"
    40  	"github.com/ethereum/go-ethereum/params"
    41  	"github.com/ethereum/go-ethereum/rpc"
    42  	"github.com/mohae/deepcopy"
    43  	"golang.org/x/net/websocket"
    44  )
    45  
    46  const (
    47  	sampleLimit = 200 // Maximum number of data samples
    48  )
    49  
    50  // Dashboard contains the dashboard internals.
    51  type Dashboard struct {
    52  	config *Config // Configuration values for the dashboard
    53  
    54  	listener   net.Listener       // Network listener listening for dashboard clients
    55  	conns      map[uint32]*client // Currently live websocket connections
    56  	nextConnID uint32             // Next connection id
    57  
    58  	history *Message // Stored historical data
    59  
    60  	lock     sync.Mutex   // Lock protecting the dashboard's internals
    61  	sysLock  sync.RWMutex // Lock protecting the stored system data
    62  	peerLock sync.RWMutex // Lock protecting the stored peer data
    63  	logLock  sync.RWMutex // Lock protecting the stored log data
    64  
    65  	geodb  *geoDB // geoip database instance for IP to geographical information conversions
    66  	logdir string // Directory containing the log files
    67  
    68  	quit chan chan error // Channel used for graceful exit
    69  	wg   sync.WaitGroup  // Wait group used to close the data collector threads
    70  }
    71  
    72  // client represents active websocket connection with a remote browser.
    73  type client struct {
    74  	conn   *websocket.Conn // Particular live websocket connection
    75  	msg    chan *Message   // Message queue for the update messages
    76  	logger log.Logger      // Logger for the particular live websocket connection
    77  }
    78  
    79  // New creates a new dashboard instance with the given configuration.
    80  func New(config *Config, commit string, logdir string) *Dashboard {
    81  	now := time.Now()
    82  	versionMeta := ""
    83  	if len(params.VersionMeta) > 0 {
    84  		versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
    85  	}
    86  	return &Dashboard{
    87  		conns:  make(map[uint32]*client),
    88  		config: config,
    89  		quit:   make(chan chan error),
    90  		history: &Message{
    91  			General: &GeneralMessage{
    92  				Commit:  commit,
    93  				Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
    94  			},
    95  			System: &SystemMessage{
    96  				ActiveMemory:   emptyChartEntries(now, sampleLimit),
    97  				VirtualMemory:  emptyChartEntries(now, sampleLimit),
    98  				NetworkIngress: emptyChartEntries(now, sampleLimit),
    99  				NetworkEgress:  emptyChartEntries(now, sampleLimit),
   100  				ProcessCPU:     emptyChartEntries(now, sampleLimit),
   101  				SystemCPU:      emptyChartEntries(now, sampleLimit),
   102  				DiskRead:       emptyChartEntries(now, sampleLimit),
   103  				DiskWrite:      emptyChartEntries(now, sampleLimit),
   104  			},
   105  		},
   106  		logdir: logdir,
   107  	}
   108  }
   109  
   110  // emptyChartEntries returns a ChartEntry array containing limit number of empty samples.
   111  func emptyChartEntries(t time.Time, limit int) ChartEntries {
   112  	ce := make(ChartEntries, limit)
   113  	for i := 0; i < limit; i++ {
   114  		ce[i] = new(ChartEntry)
   115  	}
   116  	return ce
   117  }
   118  
   119  // Protocols implements the node.Service interface.
   120  func (db *Dashboard) Protocols() []p2p.Protocol { return nil }
   121  
   122  // APIs implements the node.Service interface.
   123  func (db *Dashboard) APIs() []rpc.API { return nil }
   124  
   125  // Start starts the data collection thread and the listening server of the dashboard.
   126  // Implements the node.Service interface.
   127  func (db *Dashboard) Start(server *p2p.Server) error {
   128  	log.Info("Starting dashboard", "url", fmt.Sprintf("http://%s:%d", db.config.Host, db.config.Port))
   129  
   130  	db.wg.Add(3)
   131  	go db.collectSystemData()
   132  	go db.streamLogs()
   133  	go db.collectPeerData()
   134  
   135  	http.HandleFunc("/", db.webHandler)
   136  	http.Handle("/api", websocket.Handler(db.apiHandler))
   137  
   138  	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port))
   139  	if err != nil {
   140  		return err
   141  	}
   142  	db.listener = listener
   143  
   144  	go http.Serve(listener, nil)
   145  
   146  	return nil
   147  }
   148  
   149  // Stop stops the data collection thread and the connection listener of the dashboard.
   150  // Implements the node.Service interface.
   151  func (db *Dashboard) Stop() error {
   152  	// Close the connection listener.
   153  	var errs []error
   154  	if err := db.listener.Close(); err != nil {
   155  		errs = append(errs, err)
   156  	}
   157  	// Close the collectors.
   158  	errc := make(chan error, 1)
   159  	for i := 0; i < 3; i++ {
   160  		db.quit <- errc
   161  		if err := <-errc; err != nil {
   162  			errs = append(errs, err)
   163  		}
   164  	}
   165  	// Close the connections.
   166  	db.lock.Lock()
   167  	for _, c := range db.conns {
   168  		if err := c.conn.Close(); err != nil {
   169  			c.logger.Warn("Failed to close connection", "err", err)
   170  		}
   171  	}
   172  	db.lock.Unlock()
   173  
   174  	// Wait until every goroutine terminates.
   175  	db.wg.Wait()
   176  	log.Info("Dashboard stopped")
   177  
   178  	var err error
   179  	if len(errs) > 0 {
   180  		err = fmt.Errorf("%v", errs)
   181  	}
   182  
   183  	return err
   184  }
   185  
   186  // webHandler handles all non-api requests, simply flattening and returning the dashboard website.
   187  func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
   188  	log.Debug("Request", "URL", r.URL)
   189  
   190  	path := r.URL.String()
   191  	if path == "/" {
   192  		path = "/index.html"
   193  	}
   194  	blob, err := Asset(path[1:])
   195  	if err != nil {
   196  		log.Warn("Failed to load the asset", "path", path, "err", err)
   197  		http.Error(w, "not found", http.StatusNotFound)
   198  		return
   199  	}
   200  	w.Write(blob)
   201  }
   202  
   203  // apiHandler handles requests for the dashboard.
   204  func (db *Dashboard) apiHandler(conn *websocket.Conn) {
   205  	id := atomic.AddUint32(&db.nextConnID, 1)
   206  	client := &client{
   207  		conn:   conn,
   208  		msg:    make(chan *Message, 128),
   209  		logger: log.New("id", id),
   210  	}
   211  	done := make(chan struct{})
   212  
   213  	// Start listening for messages to send.
   214  	db.wg.Add(1)
   215  	go func() {
   216  		defer db.wg.Done()
   217  
   218  		for {
   219  			select {
   220  			case <-done:
   221  				return
   222  			case msg := <-client.msg:
   223  				if err := websocket.JSON.Send(client.conn, msg); err != nil {
   224  					client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
   225  					client.conn.Close()
   226  					return
   227  				}
   228  			}
   229  		}
   230  	}()
   231  
   232  	// Send the past data.
   233  	db.sysLock.RLock()
   234  	db.peerLock.RLock()
   235  	db.logLock.RLock()
   236  
   237  	h := deepcopy.Copy(db.history).(*Message)
   238  
   239  	db.sysLock.RUnlock()
   240  	db.peerLock.RUnlock()
   241  	db.logLock.RUnlock()
   242  
   243  	client.msg <- h
   244  
   245  	// Start tracking the connection and drop at connection loss.
   246  	db.lock.Lock()
   247  	db.conns[id] = client
   248  	db.lock.Unlock()
   249  	defer func() {
   250  		db.lock.Lock()
   251  		delete(db.conns, id)
   252  		db.lock.Unlock()
   253  	}()
   254  	for {
   255  		r := new(Request)
   256  		if err := websocket.JSON.Receive(conn, r); err != nil {
   257  			if err != io.EOF {
   258  				client.logger.Warn("Failed to receive request", "err", err)
   259  			}
   260  			close(done)
   261  			return
   262  		}
   263  		if r.Logs != nil {
   264  			db.handleLogRequest(r.Logs, client)
   265  		}
   266  	}
   267  }
   268  
   269  // sendToAll sends the given message to the active dashboards.
   270  func (db *Dashboard) sendToAll(msg *Message) {
   271  	db.lock.Lock()
   272  	for _, c := range db.conns {
   273  		select {
   274  		case c.msg <- msg:
   275  		default:
   276  			c.conn.Close()
   277  		}
   278  	}
   279  	db.lock.Unlock()
   280  }