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