diff --git a/cmd/radiod/radiod.go b/cmd/radiod/radiod.go index cadd94732e16ff09201d30fb524ad07c82a110db..c4443669d519d4288303d3adf3b76810504da76f 100644 --- a/cmd/radiod/radiod.go +++ b/cmd/radiod/radiod.go @@ -8,6 +8,9 @@ import ( "strings" "syscall" + "net/http" + _ "net/http/pprof" + "git.autistici.org/ale/autoradio" "git.autistici.org/ale/autoradio/instrumentation" "git.autistici.org/ale/autoradio/node" @@ -21,6 +24,7 @@ var ( netDev = flag.String("interface", "", "Network interface to monitor for utilization. If unset, default to the interface associated with --ip.") bwLimit = flag.Int("bwlimit", 100, "Bandwidth usage limit (Mbps)") maxClients = flag.Int("max-clients", 1000, "Maximum number of connected clients") + debugAddr = flag.String("debug-addr", "", "Set to a host:port to enable a HTTP server with debugging information") ) func shortHostname() string { @@ -54,5 +58,18 @@ func main() { }() signal.Notify(stopch, syscall.SIGTERM, syscall.SIGINT) + if *debugAddr != "" { + http.Handle("/debug/node", n) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.Redirect(w, r, "/debug/node", 302) + } + http.NotFound(w, r) + }) + go func() { + http.ListenAndServe(*debugAddr, nil) + }() + } + n.Run() } diff --git a/node/node.go b/node/node.go index 24122ade00fb314542e5576acf96c25d818d0d89..19110328c12472aff4183a4adff1237948d245cd 100644 --- a/node/node.go +++ b/node/node.go @@ -166,13 +166,18 @@ type RadioNode struct { // Node presence heartbeat. presence *presence.Presence - // How often to restart the Icecast daemon. + // Rate limiting for Icecast daemon restarts. reloadDelay time.Duration // Generator for transcodingControllers. Exposed as a member // so that it can be stubbed out during tests. transcoderFn transcodingControllerFunc + // All currently active transcoders (locked due to the + // async debugging handler). + transcodersMx sync.Mutex + transcoders map[string]*transcoder + // A note on channel types used for signaling: while I // personally prefer struct{} chans, the etcd interface for // Watch makes it convenient to use bool stop channels @@ -236,6 +241,7 @@ func NewRadioNode(name string, ips, internalIPs []net.IP, netDev string, bwLimit transcoderFn: func(p *liquidsoapParams) (transcodingController, error) { return newLiquidsoap(p) }, + transcoders: make(map[string]*transcoder), reloadDelay: 1000 * time.Millisecond, bw: bwmonitor.NewBandwidthUsageMonitor(netDev, bwLimit), maxListeners: maxListeners, @@ -286,9 +292,8 @@ func (rc *RadioNode) updater(stop chan bool) { // Keep track of all the configured transcoders (and clean // them up at the end). - transcoders := make(map[string]*transcoder) defer func() { - for _, t := range transcoders { + for _, t := range rc.transcoders { t.Stop() } }() @@ -320,8 +325,9 @@ func (rc *RadioNode) updater(stop chan bool) { // associated transcoder objects. We also need // to detect changes in the encoding params // and restart the transcoder if necessary. + rc.transcodersMx.Lock() tmp := make(map[string]struct{}) - for name := range transcoders { + for name := range rc.transcoders { tmp[name] = struct{}{} } for _, m := range rc.config.ListMounts() { @@ -330,7 +336,7 @@ func (rc *RadioNode) updater(stop chan bool) { } tparams := newLiquidsoapParams(m) - cur, ok := transcoders[m.Name] + cur, ok := rc.transcoders[m.Name] if ok { delete(tmp, m.Name) if cur.Changed(tparams) { @@ -343,14 +349,15 @@ func (rc *RadioNode) updater(stop chan bool) { rc.Log.Printf("could not create transcoder: %v", err) } else { t.Start() - transcoders[m.Name] = t + rc.transcoders[m.Name] = t } } } for name := range tmp { - transcoders[name].Stop() - delete(transcoders, name) + rc.transcoders[name].Stop() + delete(rc.transcoders, name) } + rc.transcodersMx.Unlock() // Limit the rate of reconfigurations. if rc.reloadDelay > 0 { diff --git a/node/node_debug.go b/node/node_debug.go new file mode 100644 index 0000000000000000000000000000000000000000..4f39e3c034af5d86bda006b512decd5d40f75d1b --- /dev/null +++ b/node/node_debug.go @@ -0,0 +1,96 @@ +package node + +import ( + "fmt" + "net/http" + "text/template" +) + +const debugText = `<html> +<head> + <style type="text/css"> +.info th { text-align: right; } +.error { color: red; } + </style> +</head> +<body> + <title>Node status: {{.Name}}</title> + <table class="info"> + <tr> + <th>Name:</th> + <td>{{.Name}}</td> + </tr> + <tr> + <th>Master:</th> + <td>{{if .IsMaster}}YES{{else}}NO{{end}}</td> + </tr> + <tr> + <th>Icecast:</th> + <td>{{if .IcecastUp}}OK{{else}}<span class="error">DOWN</span>{{end}}</td> + </tr> + <tr> + <th>Bandwidth:</th> + <td>{{.BandwidthUsage}}%</td> + </tr> + <tr> + <th></th> + <td></td> + </tr> + </table> + <h3>Transcoders</h3> + {{if .Transcoders}} + <table> + <tr> + <th>Source</th> + <th>Target</th> + <th>Mount</th> + <th>Format</th> + <th>Bitrate/Q</th> + </tr> + {{range .Transcoders}} + <tr> + <td>{{.SourceURL}}</td> + <td>{{.TargetIP}}:{{.TargetPort}}</td> + <td>{{.TargetMount}}</td> + <td>{{.Format}}</td> + <td>{{if gt .BitRate 0}}{{.BitRate}}{{else}}{{.Quality}}{{end}}</td> + </tr> + {{end}} + </table> + {{else}} + <p>No active transcoders on this node.</p> + {{end}} +</body> +</html>` + +var ( + debugTmpl = template.Must(template.New("node debug").Parse(debugText)) +) + +// ServeHTTP serves the debug console. +func (rc *RadioNode) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rc.transcodersMx.Lock() + var transcoders []*transcoder + for _, t := range rc.transcoders { + transcoders = append(transcoders, t) + } + rc.transcodersMx.Unlock() + + ctx := struct { + Name string + IsMaster bool + IcecastUp bool + BandwidthUsage float64 + Transcoders []*transcoder + }{ + Name: rc.name, + IsMaster: rc.me.IsMaster(), + IcecastUp: rc.icecast.GetStatus().Up, + BandwidthUsage: rc.bw.GetUsage(), + Transcoders: transcoders, + } + err := debugTmpl.Execute(w, &ctx) + if err != nil { + fmt.Fprintln(w, "debug: error executing template: ", err.Error()) + } +}