diff options
author | Sean E. Russell <ser@ser1.net> | 2020-02-28 14:38:32 -0600 |
---|---|---|
committer | Sean E. Russell <ser@ser1.net> | 2020-02-28 14:38:32 -0600 |
commit | c6af0ab404e54713f7b4039eaf9a0f21340cb00b (patch) | |
tree | f279ae965acca840ff335698287046e22f1a4a09 | |
parent | d16cf1c6d2b91f6ca75da1ded02bde25782b7a3f (diff) | |
parent | 231b0d03fed93ccc4b5f953f503763966341ec48 (diff) |
Merge branch 'v3.4.x'
51 files changed, 979 insertions, 444 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8644dd6..2f96d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,17 +23,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a histogram. - Temp widget displays degree symbol (merged from BartWillems, thanks also fleaz) +- Support for (device) plugins, and abstracting devices from widgets. This + allows adding functionality without adding bulk. ### Fixed - Keys not controlling process widget, #59 +- The one-column bug, #62 ## [3.3.2] - 2020-02-26 Bugfix release. -- Fixes #15, crash caused by battery widget when some accessories have batteries -- Fixes #57, colors with dashes in the name not found. +### Fixed + +- #15, crash caused by battery widget when some accessories have batteries +- #57, colors with dashes in the name not found. - Also, cjbassi/gotop#127 and cjbassi/gotop#130 were released back in v3.1.0. ## [3.3.1] - 2020-02-18 diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go index 5029519..0fae9e2 100644 --- a/cmd/gotop/main.go +++ b/cmd/gotop/main.go @@ -4,9 +4,11 @@ import ( "fmt" "io" "log" + "net/http" "os" "os/signal" "path/filepath" + "plugin" "strconv" "strings" "syscall" @@ -14,6 +16,7 @@ import ( docopt "github.com/docopt/docopt.go" ui "github.com/gizak/termui/v3" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/xxxserxxx/gotop" "github.com/xxxserxxx/gotop/colorschemes" @@ -25,12 +28,13 @@ import ( const ( appName = "gotop" - version = "3.3.2" + version = "3.4.0" graphHorizontalScaleDelta = 3 defaultUI = "cpu\ndisk/1 2:mem/2\ntemp\nnet procs" minimalUI = "cpu\nmem procs" batteryUI = "cpu/2 batt/1\ndisk/1 2:mem/2\ntemp\nnet procs" + procsUI = "cpu 4:procs\ndisk\nmem\nnet" ) var ( @@ -41,10 +45,10 @@ var ( stderrLogger = log.New(os.Stderr, "", 0) ) +// TODO: Add tab completion for Linux https://gist.github.com/icholy/5314423 // TODO: state:merge #135 linux console font (cmatsuoka/console-font) // TODO: state:deferred 157 FreeBSD fixes & Nvidia GPU support (kraust/master). Significant CPU use impact for NVidia changes. // TODO: Virtual devices from Prometeus metrics @feature -// TODO: Export Prometheus metrics @feature // TODO: state:merge #167 configuration file (jrswab/configFile111) func parseArgs(conf *gotop.Config) error { usage := ` @@ -63,11 +67,16 @@ Options: -b, --battery Show battery level widget ('minimal' turns off). (DEPRECATED, use -l battery) -B, --bandwidth=bits Specify the number of bits per seconds. -l, --layout=NAME Name of layout spec file for the UI. Looks first in $XDG_CONFIG_HOME/gotop, then as a path. Use "-" to pipe. - -i, --interface=NAME Select network interface [default: all]. + -i, --interface=NAME Select network interface [default: all]. Several interfaces can be defined using comma separated values. Interfaces can also be ignored using ! + -x, --export=PORT Enable metrics for export on the specified port. + -X, --extensions=NAMES Enables the listed extensions. This is a comma-separated list without the .so suffix. The current and config directories will be searched. -Several interfaces can be defined using comma separated values. -Interfaces can also be ignored using ! +Built-in layouts: + default + minimal + battery + kitchensink Colorschemes: default @@ -115,8 +124,11 @@ Colorschemes: if args["--minimal"].(bool) { conf.Layout = "minimal" } - if val, _ := args["--statusbar"]; val != nil { - rateStr, _ := args["--rate"].(string) + if val, _ := args["--export"]; val != nil { + conf.ExportPort = val.(string) + } + if val, _ := args["--rate"]; val != nil { + rateStr, _ := val.(string) rate, err := strconv.ParseFloat(rateStr, 64) if err != nil { return fmt.Errorf("invalid rate parameter") @@ -136,6 +148,10 @@ Colorschemes: if val, _ := args["--interface"]; val != nil { conf.NetInterface, _ = args["--interface"].(string) } + if val, _ := args["--extensions"]; val != nil { + exs, _ := args["--extensions"].(string) + conf.Extensions = strings.Split(exs, ",") + } return nil } @@ -335,7 +351,7 @@ func makeConfig() gotop.Config { HelpVisible: false, UpdateInterval: time.Second, AverageLoad: false, - PercpuLoad: false, + PercpuLoad: true, TempScale: w.Celsius, Statusbar: false, NetInterface: w.NET_INTERFACE_ALL, @@ -345,6 +361,7 @@ func makeConfig() gotop.Config { return conf } +// TODO: mpd visualizer widget func main() { // Set up default config conf := makeConfig() @@ -379,6 +396,8 @@ func main() { bar = w.NewStatusBar() } + loadExtensions(conf) + lstream := getLayout(conf) ly := layout.ParseLayout(lstream) grid, err := layout.Layout(ly, conf) @@ -400,6 +419,12 @@ func main() { ui.Render(bar) } + if conf.ExportPort != "" { + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(conf.ExportPort, nil) + }() + } eventLoop(conf, grid) } @@ -413,8 +438,9 @@ func getLayout(conf gotop.Config) io.Reader { return strings.NewReader(minimalUI) case "battery": return strings.NewReader(batteryUI) + case "procs": + return strings.NewReader(procsUI) default: - log.Printf("layout = %s", conf.Layout) fp := filepath.Join(conf.ConfigDir, conf.Layout) fin, err := os.Open(fp) if err != nil { @@ -426,3 +452,47 @@ func getLayout(conf gotop.Config) io.Reader { return fin } } + +func loadExtensions(conf gotop.Config) { + var hasError bool + for _, ex := range conf.Extensions { + exf := ex + ".so" + fn := exf + _, err := os.Stat(fn) + if err != nil && os.IsNotExist(err) { + log.Printf("no plugin %s found in current directory", fn) + fn = filepath.Join(conf.ConfigDir, exf) + _, err = os.Stat(fn) + if err != nil || os.IsNotExist(err) { + hasError = true + log.Printf("no plugin %s found in config directory", fn) + continue + } + } + p, err := plugin.Open(fn) + if err != nil { + hasError = true + log.Printf(err.Error()) + continue + } + init, err := p.Lookup("Init") + if err != nil { + hasError = true + log.Printf(err.Error()) + continue + } + + initFunc, ok := init.(func()) + if !ok { + hasError = true + log.Printf(err.Error()) + continue + } + initFunc() + } + if hasError { + ui.Close() + fmt.Printf("Error initializing requested plugins; check the log file %s\n", filepath.Join(conf.ConfigDir, conf.LogFile)) + os.Exit(1) + } +} diff --git a/colorschemes/default.go b/colorschemes/default.go index dc6c6b2..90ac3fa 100644 --- a/colorschemes/default.go +++ b/colorschemes/default.go @@ -12,8 +12,7 @@ func init() { BattLines: []int{4, 3, 2, 1, 5, 6, 7, 8}, - MainMem: 5, - SwapMem: 11, + MemLines: []int{5, 11, 4, 3, 2, 1, 6, 7, 8}, ProcCursor: 4, diff --git a/colorschemes/default.json b/colorschemes/default.json index 12cab1b..d6b3dc5 100644 --- a/colorschemes/default.json +++ b/colorschemes/default.json @@ -11,8 +11,7 @@ "BattLines": [4, 3, 2, 1, 5, 6, 7, 8], - "MainMem": 5, - "SwapMem": 11, + "MemLines": [5, 11, 4, 3, 2, 1, 6, 7, 8], "ProcCursor": 4, diff --git a/colorschemes/default_dark.go b/colorschemes/default_dark.go index 849f85b..7f7911c 100644 --- a/colorschemes/default_dark.go +++ b/colorschemes/default_dark.go @@ -12,8 +12,7 @@ func init() { BattLines: []int{4, 3, 2, 1, 5, 6, 7, 8}, - MainMem: 5, - SwapMem: 3, + MemLines: []int{5, 3, 4, 2, 1, 6, 7, 8, 11}, ProcCursor: 33, diff --git a/colorschemes/monokai.go b/colorschemes/monokai.go index cd0471c..d09f3c9 100644 --- a/colorschemes/monokai.go +++ b/colorschemes/monokai.go @@ -12,8 +12,7 @@ func init() { BattLines: []int{81, 70, 208, 197, 249, 141, 221, 186}, - MainMem: 208, - SwapMem: 186, + MemLines: []int{208, 186, 81, 70, 208, 197, 249, 141, 221, 186}, ProcCursor: 197, diff --git a/colorschemes/nord.go b/colorschemes/nord.go index b6d4b50..028057e 100644 --- a/colorschemes/nord.go +++ b/colorschemes/nord.go @@ -25,8 +25,7 @@ func init() { BattLines: []int{4, 3, 2, 1, 5, 6, 7, 8}, - MainMem: 172, // Orange - SwapMem: 221, // yellow + MemLines: []int{172, 221, 4, 3, 2, 1, 5, 6, 7, 8}, ProcCursor: 31, // blue (nord9) diff --git a/colorschemes/solarized.go b/colorschemes/solarized.go index 48d67b0..4649d93 100644 --- a/colorschemes/solarized.go +++ b/colorschemes/solarized.go @@ -15,8 +15,7 @@ func init() { BattLines: []int{61, 33, 37, 64, 125, 160, 166, 136}, - MainMem: 125, - SwapMem: 166, + MemLines: []int{125, 166, 61, 33, 37, 64, 125, 160, 166, 136}, ProcCursor: 136, diff --git a/colorschemes/solarized16_dark.go b/colorschemes/solarized16_dark.go index 354991b..00c7eb6 100644 --- a/colorschemes/solarized16_dark.go +++ b/colorschemes/solarized16_dark.go @@ -14,8 +14,7 @@ func init() { BattLines: []int{13, 4, 6, 2, 5, 1, 9, 3}, - MainMem: 5, - SwapMem: 9, + MemLines: []int{5, 9, 13, 4, 6, 2, 1, 3}, ProcCursor: 4, diff --git a/colorschemes/solarized16_light.go b/colorschemes/solarized16_light.go index 0fae397..b4157d9 100644 --- a/colorschemes/solarized16_light.go +++ b/colorschemes/solarized16_light.go @@ -14,8 +14,7 @@ func init() { BattLines: []int{13, 4, 6, 2, 5, 1, 9, 3}, - MainMem: 5, - SwapMem: 9, + MemLines: []int{5, 9, 13, 4, 6, 2, 1, 3}, ProcCursor: 4, diff --git a/colorschemes/template.go b/colorschemes/template.go index 9b7ac79..af4b792 100644 --- a/colorschemes/template.go +++ b/colorschemes/template.go @@ -32,8 +32,7 @@ type Colorscheme struct { BattLines []int - MainMem int - SwapMem int + MemLines []int ProcCursor int diff --git a/colorschemes/vice.go b/colorschemes/vice.go index 8bd2545..d0dbb03 100644 --- a/colorschemes/vice.go +++ b/colorschemes/vice.go @@ -12,8 +12,7 @@ func init() { BattLines: []int{212, 218, 123, 159, 229, 158, 183, 146}, - MainMem: 201, - SwapMem: 97, + MemLines: []int{201, 97, 212, 218, 123, 159, 229, 158, 183, 146}, ProcCursor: 159, @@ -30,6 +30,8 @@ type Config struct { NetInterface string Layout string MaxLogSize int64 + ExportPort string + Extensions []string } func Parse(in io.Reader, conf *Config) error { @@ -109,6 +111,10 @@ func Parse(in io.Reader, conf *Config) error { return err } conf.MaxLogSize = int64(iv) + case "export": + conf.ExportPort = kv[1] + case "extensions": + conf.Extensions = strings.Split(kv[1], ",") } } diff --git a/devices/cpu.go b/devices/cpu.go new file mode 100644 index 0000000..436ccb1 --- /dev/null +++ b/devices/cpu.go @@ -0,0 +1,34 @@ +package devices + +import ( + "log" + "time" +) + +var cpuFuncs []func(map[string]int, time.Duration, bool) map[string]error + +// RegisterCPU adds a new CPU device to the CPU widget. labels returns the +// names of the devices; they should be as short as possible, and the indexes +// of the returned slice should align with the values returned by the percents +// function. The percents function should return the percent CPU usage of the +// device(s), sliced over the time duration supplied. If the bool argument to +// percents is true, it is expected that the return slice +// +// labels may be called once and the value cached. This means the number of +// cores should not change dynamically. +func RegisterCPU(f func(map[string]int, time.Duration, bool) map[string]error) { + cpuFuncs = append(cpuFuncs, f) +} + +// CPUPercent calculates the percentage of cpu used either per CPU or combined. +// Returns one value per cpu, or a single value if percpu is set to false. +func UpdateCPU(cpus map[string]int, interval time.Duration, logical bool) { + for _, f := range cpuFuncs { + errs := f(cpus, interval, logical) + if errs != nil { + for k, e := range errs { + log.Printf("%s: %s", k, e) + } + } + } +} diff --git a/devices/cpu_cpu.go b/devices/cpu_cpu.go new file mode 100644 index 0000000..a1d20cd --- /dev/null +++ b/devices/cpu_cpu.go @@ -0,0 +1,31 @@ +package devices + +import ( + "fmt" + "time" + + psCpu "github.com/shirou/gopsutil/cpu" +) + +func init() { + f := func(cpus map[string]int, iv time.Duration, l bool) map[string]error { + cpuCount, err := psCpu.Counts(l) + if err != nil { + return nil + } + formatString := "CPU%1d" + if cpuCount > 10 { + formatString = "CPU%02d" + } + vals, err := psCpu.Percent(iv, l) + if err != nil { + return map[string]error{"gopsutil": err} + } + for i := 0; i < len(vals); i++ { + key := fmt.Sprintf(formatString, i) + cpus[key] = int(vals[i]) + } + return nil + } + RegisterCPU(f) +} diff --git a/devices/devices.go b/devices/devices.go new file mode 100644 index 0000000..91a8815 --- /dev/null +++ b/devices/devices.go @@ -0,0 +1,26 @@ +package devices + +import "log" + +var shutdownFuncs []func() error + +// RegisterShutdown stores a function to be called by gotop on exit, allowing +// extensions to properly release resources. Extensions should register a +// shutdown function IFF the extension is using resources that need to be +// released. The returned error will be logged, but no other action will be +// taken. +func RegisterShutdown(f func() error) { + shutdownFuncs = append(shutdownFuncs, f) +} + +// Shutdown will be called by the `main()` function if gotop is exited +// cleanly. It will call all of the registered shutdown functions of devices, +// logging all errors but otherwise not responding to them. +func Shutdown() { + for _, f := range shutdownFuncs { + err := f() + if err != nil { + log.Print(err) + } + } +} diff --git a/widgets/include/smc.c b/devices/include/smc.c index 0c200d6..0c200d6 100644 --- a/widgets/include/smc.c +++ b/devices/include/smc.c diff --git a/widgets/include/smc.h b/devices/include/smc.h index d837e11..d837e11 100644 --- a/widgets/include/smc.h +++ b/devices/include/smc.h diff --git a/devices/mem.go b/devices/mem.go new file mode 100644 index 0000000..defb01f --- /dev/null +++ b/devices/mem.go @@ -0,0 +1,26 @@ +package devices + +import "log" + +var memFuncs []func(map[string]MemoryInfo) map[string]error + +type MemoryInfo struct { + Total uint64 + Used uint64 + UsedPercent float64 +} + +func RegisterMem(f func(map[string]MemoryInfo) map[string]error) { + memFuncs = append(memFuncs, f) +} + +func UpdateMem(mem map[string]MemoryInfo) { + for _, f := range memFuncs { + errs := f(mem) + if errs != nil { + for k, e := range errs { + log.Printf("%s: %s", k, e) + } + } + } +} diff --git a/devices/mem_mem.go b/devices/mem_mem.go new file mode 100644 index 0000000..53e5721 --- /dev/null +++ b/devices/mem_mem.go @@ -0,0 +1,21 @@ +package devices + +import ( + psMem "github.com/shirou/gopsutil/mem" +) + +func init() { + mf := func(mems map[string]MemoryInfo) map[string]error { + mainMemory, err := psMem.VirtualMemory() + if err != nil { + return map[string]error{"Main": err} + } + mems["Main"] = MemoryInfo{ + Total: mainMemory.Total, + Used: mainMemory.Used, + UsedPercent: mainMemory.UsedPercent, + } + return nil + } + RegisterMem(mf) +} diff --git a/devices/mem_swap_freebsd.go b/devices/mem_swap_freebsd.go new file mode 100644 index 0000000..3a95aa9 --- /dev/null +++ b/devices/mem_swap_freebsd.go @@ -0,0 +1,44 @@ +// +build freebsd + +package devices + +import ( + "os/exec" + "strconv" + "strings" +) + +func init() { + mf := func(mems map[string]MemoryInfo) map[string]error { + cmd := "swapinfo -k|sed -n '1!p'|awk '{print $2,$3,$5}'" + output, err := exec.Command("sh", "-c", cmd).Output() + if err != nil { + return map[string]error{"swapinfo": err} + } + + s := strings.TrimSuffix(string(output), "\n") + s = strings.ReplaceAll(s, "\n", " ") + ss := strings.Split(s, " ") + ss = ss[((len(ss)/3)-1)*3:] + + errors := make(map[string]error) + mem := MemoryInfo{} + mem.Total, err = strconv.ParseUint(ss[0], 10, 64) + if err != nil { + errors["swap total"] = err + } + + mem.Used, err = strconv.ParseUint(ss[1], 10, 64) + if err != nil { + errors["swap used"] = err + } + + mem.UsedPercent, err = strconv.ParseFloat(strings.TrimSuffix(ss[2], "%"), 64) + if err != nil { + errors["swap percent"] = err + } + mems["Swap"] = mem + return errors + } + RegisterMem(mf) +} diff --git a/devices/mem_swap_other.go b/devices/mem_swap_other.go new file mode 100644 index 0000000..fb16705 --- /dev/null +++ b/devices/mem_swap_other.go @@ -0,0 +1,23 @@ +// +build !freebsd + +package devices + +import ( + psMem "github.com/shirou/gopsutil/mem" +) + +func init() { + mf := func(mems map[string]MemoryInfo) map[string]error { + memory, err := psMem.SwapMemory() + if err != nil { + return map[string]error{"Swap": err} + } + mems["Swap"] = MemoryInfo{ + Total: memory.Total, + Used: memory.Used, + UsedPercent: memory.UsedPercent, + } + return nil + } + RegisterMem(mf) +} diff --git a/devices/temp.go b/devices/temp.go new file mode 100644 index 0000000..010e7ad --- /dev/null +++ b/devices/temp.go @@ -0,0 +1,22 @@ +package devices + +import ( + "log" +) + +var tempUpdates []func(map[string]int) map[string]error + +func RegisterTemp(update func(map[string]int) map[string]error) { + tempUpdates = append(tempUpdates, update) +} + +func UpdateTemps(temps map[string]int) { + for _, f := range tempUpdates { + errs := f(temps) + if errs != nil { + for k, e := range errs { + log.Printf("error updating temp for %s: %s", k, e) + } + } + } +} diff --git a/widgets/temp_darwin.go b/devices/temp_darwin.go index d0e512b..e60b4be 100644 --- a/widgets/temp_darwin.go +++ b/devices/temp_darwin.go @@ -1,22 +1,16 @@ // +build darwin -package widgets +package devices // #cgo LDFLAGS: -framework IOKit // #include "include/smc.c" import "C" -import ( - "log" - "github.com/xxxserxxx/gotop/utils" -) - -type TemperatureStat struct { - SensorKey string `json:"sensorKey"` - Temperature float64 `json:"sensorTemperature"` +func init() { + RegisterTemp(update) } -func SensorsTemperatures() ([]TemperatureStat, error) { +func update(temps map[string]int) map[string]error { temperatureKeys := map[string]string{ C.AMBIENT_AIR_0: "ambient_air_0", C.AMBIENT_AIR_1: "ambient_air_1", @@ -41,34 +35,12 @@ func SensorsTemperatures() ([]TemperatureStat, error) { C.WIRELESS_MODULE: "wireless_module", } - var temperatures []TemperatureStat - C.open_smc() defer C.close_smc() for key, val := range temperatureKeys { - temperatures = append(temperatures, TemperatureStat{ - SensorKey: val, - Temperature: float64(C.get_tmp(C.CString(key), C.CELSIUS)), - }) + temps[val] = int(C.get_tmp(C.CString(key), C.CELSIUS)) |