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 }