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