package devices import ( "bufio" "log" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/droundy/goopt" ) var nameP *string var remoteUrlP *string var sleepP *string var sleep time.Duration var remoteLock sync.Mutex // FIXME Widgets don't align values // TODO remote network & disk aren't reported // TODO network resiliency; I believe it currently crashes gotop when the network goes down // TODO Replace custom decoder with https://github.com/prometheus/common/blob/master/expfmt/decode.go // FIXME high CPU use when remote goes offline // FIXME higher CPU use when using remote in general func init() { nameP = goopt.String([]string{"--remote-name"}, "", "Remote: name of remote gotop") remoteUrlP = goopt.String([]string{"--remote-url"}, "", "Remote: URL of remote gotop") sleepP = goopt.String([]string{"--remote-refresh"}, "", "Remote: Frequency to refresh data, in seconds") RegisterStartup(startup) } type Remote struct { url string refresh time.Duration } func startup(vars map[string]string) error { _cpuData = make(map[string]int) _tempData = make(map[string]int) _netData = make(map[string]float64) _diskData = make(map[string]float64) _memData = make(map[string]MemoryInfo) remoteLock = sync.Mutex{} remotes := parseConfig(vars) if remoteUrlP != nil && *remoteUrlP != "" { var name string r := Remote{ url: *remoteUrlP, refresh: 5 * time.Second, } if nameP == nil && *nameP != "" { name = *nameP } else { name = "Remote" } if sleepP == nil && *sleepP != "" { sleep, err := time.ParseDuration(*sleepP) if err == nil { r.refresh = sleep } else { log.Printf("invalid refresh duration %s for %s; using default", *sleepP, *remoteUrlP) } } remotes[name] = r } if len(remotes) == 0 { log.Println("Remote: no remote URL provided; disabling extension") return nil } RegisterTemp(updateTemp) RegisterMem(updateMem) RegisterCPU(updateUsage) // We need to know what we're dealing with, so the following code does two // things, one of them sneakily. It forks off background processes // to periodically pull data from remote sources and cache the results for // when the UI wants it. When it's run the first time, it sets up a WaitGroup // so that it can hold off returning until it's received data from the remote // so that the rest of the program knows how many cores, disks, etc. it needs // to set up UI elements for. After the first run, each process discards the // the wait group. w := &sync.WaitGroup{} for n, r := range remotes { n = n + "-" var u *url.URL w.Add(1) go func(name string, remote Remote, wg *sync.WaitGroup) { for { res, err := http.Get(remote.url) if err == nil { u, err = url.Parse(remote.url) if err == nil { if res.StatusCode == http.StatusOK { bi := bufio.NewScanner(res.Body) process(name, bi) } else { u.User = nil log.Printf("unsuccessful connection to %s: http status %s", u.String(), res.Status) } } else { log.Print("error processing remote URL") } } else { } res.Body.Close() if wg != nil { wg.Done() wg = nil } time.Sleep(remote.refresh) } }(n, r, w) } w.Wait() return nil } var ( _cpuData map[string]int _tempData map[string]int _netData map[string]float64 _diskData map[string]float64 _memData map[string]MemoryInfo ) func process(host string, data *bufio.Scanner) { remoteLock.Lock() for data.Scan() { line := data.Text() if line[0] == '#' { continue } if line[0:6] != _gotop { continue } sub := line[6:] switch { case strings.HasPrefix(sub, _cpu): // int gotop_cpu_CPU0 procInt(host, line, sub[4:], _cpuData) case strings.HasPrefix(sub, _temp): // int gotop_temp_acpitz procInt(host, line, sub[5:], _tempData) case strings.HasPrefix(sub, _net): // int gotop_net_recv parts := strings.Split(sub[5:], " ") if len(parts) < 2 { log.Printf(`bad data; not enough columns in "%s"`, line) continue } val, err := strconv.ParseFloat(parts[1], 64) if err != nil { log.Print(err) continue } _netData[host+parts[0]] = val case strings.HasPrefix(sub, _disk): // float % gotop_disk_:dev:mmcblk0p1 parts := strings.Split(sub[5:], " ") if len(parts) < 2 { log.Printf(`bad data; not enough columns in "%s"`, line) continue } val, err := strconv.ParseFloat(parts[1], 64) if err != nil { log.Print(err) continue } _diskData[host+parts[0]] = val case strings.HasPrefix(sub, _mem): // float % gotop_memory_Main parts := strings.Split(sub[7:], " ") if len(parts) < 2 { log.Printf(`bad data; not enough columns in "%s"`, line) continue } val, err := strconv.ParseFloat(parts[1], 64) if err != nil { log.Print(err) continue } _memData[host+parts[0]] = MemoryInfo{ Total: 100, Used: uint64(100.0 / val), UsedPercent: val, } default: // NOP! This is a metric we don't care about. } } remoteLock.Unlock() } func procInt(host, line, sub string, data map[string]int) { parts := strings.Split(sub, " ") if len(parts) < 2 { log.Printf(`bad data; not enough columns in "%s"`, line) return } val, err := strconv.Atoi(parts[1]) if err != nil { log.Print(err) return } data[host+parts[0]] = val } func updateTemp(temps map[string]int) map[string]error { remoteLock.Lock() for name, val := range _tempData { temps[name] = val } remoteLock.Unlock() return nil } // FIXME The units are wrong: getting bytes, assuming they're % func updateMem(mems map[string]MemoryInfo) map[string]error { remoteLock.Lock() for name, val := range _memData { mems[name] = val } remoteLock.Unlock() return nil } func updateUsage(cpus map[string]int, _ bool) map[string]error { remoteLock.Lock() for name, val := range _cpuData { cpus[name] = val } remoteLock.Unlock() return nil } func parseConfig(vars map[string]string) map[string]Remote { rv := make(map[string]Remote) for key, value := range vars { if strings.HasPrefix(key, "remote-") { parts := strings.Split(key, "-") if len(parts) == 2 { log.Printf("malformed Remote extension configuration '%s'; must be 'remote-NAME-url' or 'remote-NAME-refresh'", key) continue } name := parts[1] remote, ok := rv[name] if !ok { remote = Remote{} } if parts[2] == "url" { remote.url = value } else if parts[2] == "refresh" { sleep, err := strconv.Atoi(value) if err != nil { log.Printf("illegal Remote extension value for %s: '%s'. Must be a duration in seconds, e.g. '2'", key, value) continue } remote.refresh = time.Duration(sleep) * time.Second } else { log.Printf("bad configuration option for Remote extension: '%s'; must be 'remote-NAME-url' or 'remote-NAME-refresh'", key) continue } rv[name] = remote } } return rv } const ( _gotop = "gotop_" _cpu = "cpu_" _temp = "temp_" _net = "net_" _disk = "disk_" _mem = "memory_" )