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 }