xmtop/cmd/gotop/main.go

592 lines
15 KiB
Go
Raw Normal View History

2018-02-19 15:25:02 +08:00
package main
import (
2018-12-11 13:21:40 +08:00
"fmt"
"io"
"log"
"net/http"
2018-02-19 15:25:02 +08:00
"os"
"os/signal"
"path/filepath"
2020-02-29 00:03:41 +08:00
"plugin"
2018-03-09 16:27:46 +08:00
"strconv"
"strings"
2018-02-19 15:25:02 +08:00
"syscall"
"time"
2019-02-07 09:18:44 +08:00
docopt "github.com/docopt/docopt.go"
2019-03-08 15:15:41 +08:00
ui "github.com/gizak/termui/v3"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/shibukawa/configdir"
2019-02-07 09:18:44 +08:00
2020-02-29 04:48:35 +08:00
"github.com/xxxserxxx/gotop/v3"
"github.com/xxxserxxx/gotop/v3/colorschemes"
"github.com/xxxserxxx/gotop/v3/layout"
"github.com/xxxserxxx/gotop/v3/logging"
w "github.com/xxxserxxx/gotop/v3/widgets"
2018-02-19 15:25:02 +08:00
)
2019-01-13 08:31:37 +08:00
const (
2019-02-16 18:11:05 +08:00
appName = "gotop"
2019-01-13 08:31:37 +08:00
graphHorizontalScaleDelta = 3
defaultUI = "2:cpu\ndisk/1 2:mem/2\ntemp\n2:net 2:procs"
minimalUI = "cpu\nmem procs"
batteryUI = "cpu/2 batt/1\ndisk/1 2:mem/2\ntemp\nnet procs"
2020-02-29 00:03:41 +08:00
procsUI = "cpu 4:procs\ndisk\nmem\nnet"
2020-03-07 19:01:47 +08:00
kitchensink = "3:cpu/2 3:mem/1\n4:temp/1 3:disk/2\npower\n3:net 3:procs"
2019-01-13 08:31:37 +08:00
)
2018-02-19 15:25:02 +08:00
var (
// TODO: Set this at compile time; having to check this in sucks.
2020-03-06 21:29:23 +08:00
Version = "3.5.0"
2020-02-14 00:15:52 +08:00
conf gotop.Config
help *w.HelpMenu
bar *w.StatusBar
statusbar bool
stderrLogger = log.New(os.Stderr, "", 0)
2018-02-19 15:25:02 +08:00
)
2020-02-20 00:40:45 +08:00
// TODO: Add tab completion for Linux https://gist.github.com/icholy/5314423
2020-02-21 08:56:52 +08:00
// 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: Abstract out the UI toolkit. mum4k/termdash, VladimirMarkelov/clui, gcla/gowid, rivo/tview, marcusolsson/tui-go might work better for some OS/Archs. Performance/memory use comparison would be interesting.
func parseArgs(conf *gotop.Config) error {
2020-04-07 00:04:08 +08:00
cds := conf.ConfigDir.QueryFolders(configdir.All)
cpaths := make([]string, len(cds))
for i, p := range cds {
cpaths[i] = p.Path
}
usage := fmt.Sprintf(`
2018-02-19 15:25:02 +08:00
Usage: gotop [options]
Options:
-c, --color=NAME Set a colorscheme.
-h, --help Show this screen.
-m, --minimal Only show CPU, Mem and Process widgets. Overrides -l. (DEPRECATED, use -l minimal)
-r, --rate=RATE Number of times per second to update CPU and Mem widgets [default: 1].
-V, --version Print version and exit.
-p, --percpu Show each CPU in the CPU widget.
-a, --averagecpu Show average CPU in the CPU widget.
-f, --fahrenheit Show temperatures in fahrenheit.
-s, --statusbar Show a statusbar with the time.
-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.
2020-02-29 00:03:41 +08:00
-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.
2020-02-29 00:03:41 +08:00
-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.
--test Runs tests and exits with success/failure code
2020-03-08 02:14:19 +08:00
--print-paths List out the paths that gotop will look for gotop.conf, layouts, color schemes, and extensions
2020-03-08 21:50:20 +08:00
--print-keys Show the keyboard bindings
2020-04-07 00:04:08 +08:00
Colorschemes and layouts that are not built-in are searched for (in order) in:
%s
The first path in this list is always the cwd.
2020-02-29 00:03:41 +08:00
Built-in layouts:
default
minimal
battery
kitchensink
2018-02-19 15:25:02 +08:00
Colorschemes:
default
2018-04-10 13:00:27 +08:00
default-dark (for white background)
2018-02-21 18:24:36 +08:00
solarized
solarized16-dark
solarized16-light
2018-02-22 11:54:25 +08:00
monokai
2019-02-21 15:51:49 +08:00
vice
2020-04-07 00:04:08 +08:00
`, strings.Join(cpaths, ", "))
2018-02-19 15:25:02 +08:00
var err error
conf.Colorscheme, err = colorschemes.FromName(conf.ConfigDir, "default")
if err != nil {
return err
}
args, err := docopt.ParseArgs(usage, os.Args[1:], Version)
if err != nil {
return err
}
2018-02-21 18:24:36 +08:00
if val, _ := args["--layout"]; val != nil {
conf.Layout = val.(string)
}
2018-02-21 18:24:36 +08:00
if val, _ := args["--color"]; val != nil {
cs, err := colorschemes.FromName(conf.ConfigDir, val.(string))
if err != nil {
return err
2018-12-11 13:21:40 +08:00
}
conf.Colorscheme = cs
2018-02-21 18:24:36 +08:00
}
if args["--averagecpu"].(bool) {
conf.AverageLoad, _ = args["--averagecpu"].(bool)
}
if args["--percpu"].(bool) {
conf.PercpuLoad, _ = args["--percpu"].(bool)
}
if args["--statusbar"].(bool) {
statusbar, _ = args["--statusbar"].(bool)
}
if args["--battery"].(bool) {
conf.Layout = "battery"
2018-03-09 16:34:26 +08:00
}
if args["--minimal"].(bool) {
conf.Layout = "minimal"
2019-03-01 08:29:52 +08:00
}
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")
}
if rate < 1 {
conf.UpdateInterval = time.Second * time.Duration(1/rate)
} else {
conf.UpdateInterval = time.Second / time.Duration(rate)
2018-12-11 13:21:40 +08:00
}
}
if val, _ := args["--fahrenheit"]; val != nil {
fahrenheit, _ := val.(bool)
if fahrenheit {
conf.TempScale = w.Fahrenheit
}
2018-02-19 15:25:02 +08:00
}
if val, _ := args["--interface"]; val != nil {
conf.NetInterface, _ = args["--interface"].(string)
}
2020-02-29 00:03:41 +08:00
if val, _ := args["--extensions"]; val != nil {
exs, _ := args["--extensions"].(string)
conf.Extensions = strings.Split(exs, ",")
}
if val, _ := args["--test"]; val != nil {
conf.Test = val.(bool)
}
if args["--print-paths"].(bool) {
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.All) {
paths = append(paths, d.Path)
}
fmt.Println(strings.Join(paths, "\n"))
os.Exit(0)
}
2020-03-08 02:14:19 +08:00
if args["--print-keys"].(bool) {
fmt.Println(`
Quit: q or <C-c>
Process navigation:
k and <Up>: up
j and <Down>: down
<C-u>: half page up
<C-d>: half page down
<C-b>: full page up
<C-f>: full page down
gg and <Home>: jump to top
G and <End>: jump to bottom
Process actions:
<Tab>: toggle process grouping
dd: kill selected process or group of processes with SIGTERM
d3: kill selected process or group of processes with SIGQUIT
d9: kill selected process or group of processes with SIGKILL
Process sorting
c: CPU
m: Mem
p: PID
Process filtering:
/: start editing filter
(while editing):
<Enter> accept filter
<C-c> and <Escape>: clear filter
CPU and Mem graph scaling:
h: scale in
l: scale out
?: toggles keybind help menu`)
os.Exit(0)
}
return nil
2018-02-19 15:25:02 +08:00
}
2020-02-14 00:15:52 +08:00
func setDefaultTermuiColors(c gotop.Config) {
ui.Theme.Default = ui.NewStyle(ui.Color(c.Colorscheme.Fg), ui.Color(c.Colorscheme.Bg))
ui.Theme.Block.Title = ui.NewStyle(ui.Color(c.Colorscheme.BorderLabel), ui.Color(c.Colorscheme.Bg))
ui.Theme.Block.Border = ui.NewStyle(ui.Color(c.Colorscheme.BorderLine), ui.Color(c.Colorscheme.Bg))
2018-02-19 15:25:02 +08:00
}
2020-02-14 00:15:52 +08:00
func eventLoop(c gotop.Config, grid *layout.MyGrid) {
drawTicker := time.NewTicker(c.UpdateInterval).C
2018-02-19 15:25:02 +08:00
// handles kill signal sent to gotop
sigTerm := make(chan os.Signal, 2)
signal.Notify(sigTerm, os.Interrupt, syscall.SIGTERM)
2018-02-19 15:25:02 +08:00
uiEvents := ui.PollEvents()
2018-02-21 19:41:40 +08:00
previousKey := ""
2018-02-19 15:25:02 +08:00
for {
select {
case <-sigTerm:
return
case <-drawTicker:
if !c.HelpVisible {
ui.Render(grid)
2019-03-17 22:54:56 +08:00
if statusbar {
ui.Render(bar)
}
}
case e := <-uiEvents:
if grid.Proc != nil && grid.Proc.HandleEvent(e) {
ui.Render(grid.Proc)
break
}
switch e.ID {
case "q", "<C-c>":
return
case "?":
c.HelpVisible = !c.HelpVisible
case "<Resize>":
payload := e.Payload.(ui.Resize)
2019-02-07 09:18:44 +08:00
termWidth, termHeight := payload.Width, payload.Height
2019-02-02 15:22:27 +08:00
if statusbar {
2019-02-07 09:18:44 +08:00
grid.SetRect(0, 0, termWidth, termHeight-1)
bar.SetRect(0, termHeight-1, termWidth, termHeight)
2019-02-02 15:22:27 +08:00
} else {
grid.SetRect(0, 0, payload.Width, payload.Height)
}
help.Resize(payload.Width, payload.Height)
ui.Clear()
}
if c.HelpVisible {
switch e.ID {
case "?":
2018-02-19 15:25:02 +08:00
ui.Clear()
ui.Render(help)
case "<Escape>":
c.HelpVisible = false
ui.Render(grid)
case "<Resize>":
ui.Render(help)
2018-02-19 15:25:02 +08:00
}
} else {
switch e.ID {
case "?":
ui.Render(grid)
case "h":
c.GraphHorizontalScale += graphHorizontalScaleDelta
for _, item := range grid.Lines {
item.Scale(c.GraphHorizontalScale)
}
ui.Render(grid)
case "l":
if c.GraphHorizontalScale > graphHorizontalScaleDelta {
c.GraphHorizontalScale -= graphHorizontalScaleDelta
for _, item := range grid.Lines {
item.Scale(c.GraphHorizontalScale)
ui.Render(item)
}
}
case "<Resize>":
ui.Render(grid)
2019-02-06 18:11:41 +08:00
if statusbar {
ui.Render(bar)
2019-02-06 18:11:41 +08:00
}
case "<MouseLeft>":
if grid.Proc != nil {
payload := e.Payload.(ui.Mouse)
grid.Proc.HandleClick(payload.X, payload.Y)
ui.Render(grid.Proc)
}
case "k", "<Up>", "<MouseWheelUp>":
if grid.Proc != nil {
grid.Proc.ScrollUp()
ui.Render(grid.Proc)
}
case "j", "<Down>", "<MouseWheelDown>":
if grid.Proc != nil {
grid.Proc.ScrollDown()
ui.Render(grid.Proc)
}
2019-02-02 15:22:27 +08:00
case "<Home>":
if grid.Proc != nil {
grid.Proc.ScrollTop()
ui.Render(grid.Proc)
}
2019-02-02 15:06:57 +08:00
case "g":
if grid.Proc != nil {
if previousKey == "g" {
grid.Proc.ScrollTop()
ui.Render(grid.Proc)
}
}
case "G", "<End>":
if grid.Proc != nil {
grid.Proc.ScrollBottom()
ui.Render(grid.Proc)
}
case "<C-d>":
if grid.Proc != nil {
grid.Proc.ScrollHalfPageDown()
ui.Render(grid.Proc)
}
case "<C-u>":
if grid.Proc != nil {
grid.Proc.ScrollHalfPageUp()
ui.Render(grid.Proc)
}
case "<C-f>":
if grid.Proc != nil {
grid.Proc.ScrollPageDown()
ui.Render(grid.Proc)
}
case "<C-b>":
if grid.Proc != nil {
grid.Proc.ScrollPageUp()
ui.Render(grid.Proc)
}
case "d":
if grid.Proc != nil {
if previousKey == "d" {
grid.Proc.KillProc("SIGTERM")
}
}
case "3":
if grid.Proc != nil {
if previousKey == "d" {
grid.Proc.KillProc("SIGQUIT")
}
}
case "9":
if grid.Proc != nil {
if previousKey == "d" {
grid.Proc.KillProc("SIGKILL")
}
}
case "<Tab>":
if grid.Proc != nil {
grid.Proc.ToggleShowingGroupedProcs()
ui.Render(grid.Proc)
}
case "m", "c", "p":
if grid.Proc != nil {
grid.Proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID))
ui.Render(grid.Proc)
}
case "/":
if grid.Proc != nil {
grid.Proc.SetEditingFilter(true)
ui.Render(grid.Proc)
}
}
if previousKey == e.ID {
previousKey = ""
} else {
previousKey = e.ID
2018-02-19 15:25:02 +08:00
}
}
2018-12-05 13:07:14 +08:00
2018-02-19 15:25:02 +08:00
}
}
}
2018-02-19 15:25:02 +08:00
func makeConfig() gotop.Config {
cd := configdir.New("", appName)
cd.LocalPath, _ = filepath.Abs(".")
conf = gotop.Config{
ConfigDir: cd,
GraphHorizontalScale: 7,
HelpVisible: false,
UpdateInterval: time.Second,
AverageLoad: false,
PercpuLoad: true,
TempScale: w.Celsius,
Statusbar: false,
NetInterface: w.NET_INTERFACE_ALL,
MaxLogSize: 5000000,
Layout: "default",
}
return conf
}
2020-02-20 00:40:45 +08:00
// TODO: mpd visualizer widget
2018-12-10 13:19:09 +08:00
func main() {
// This is just to make sure gotop returns a useful exit code, but also
// executes all defer statements and so cleans up before exit. Sort of
// annoying work-around for a lack of a clean way to exit Go programs
// with exit codes.
ec := run()
2020-03-08 21:50:20 +08:00
if ec > 0 {
if ec < 2 {
fmt.Printf("errors encountered; check the log file %s\n", filepath.Join(conf.ConfigDir.QueryCacheFolder().Path, logging.LOGFILE))
}
}
os.Exit(ec)
}
func run() int {
// Set up default config
conf := makeConfig()
// Find the config file; look in (1) local, (2) user, (3) global
err := conf.Load()
if err != nil {
2020-03-08 21:50:20 +08:00
fmt.Printf("failed to parse config file: %s\n", err)
return 2
}
// Override with command line arguments
err = parseArgs(&conf)
if err != nil {
2020-03-08 21:50:20 +08:00
fmt.Printf("parsing CLI args: %s\n", err)
return 2
2018-12-11 13:21:40 +08:00
}
2020-02-16 04:27:31 +08:00
logfile, err := logging.New(conf)
2019-01-20 11:37:31 +08:00
if err != nil {
fmt.Printf("failed to setup log file: %v\n", err)
2020-03-08 21:50:20 +08:00
return 2
2019-01-20 11:37:31 +08:00
}
2019-02-07 09:18:44 +08:00
defer logfile.Close()
2019-01-20 11:37:31 +08:00
lstream, err := getLayout(conf)
if err != nil {
2020-03-08 21:50:20 +08:00
stderrLogger.Print(err)
return 1
}
ly := layout.ParseLayout(lstream)
err = loadExtensions(conf)
if err != nil {
2020-03-08 21:50:20 +08:00
stderrLogger.Print(err)
return 1
}
if conf.Test {
return runTests(conf)
}
2018-12-10 13:19:09 +08:00
if err := ui.Init(); err != nil {
2020-03-08 21:50:20 +08:00
stderrLogger.Print(err)
return 1
}
defer ui.Close()
2018-12-11 13:21:40 +08:00
setDefaultTermuiColors(conf) // done before initializing widgets to allow inheriting colors
help = w.NewHelpMenu()
if statusbar {
bar = w.NewStatusBar()
}
2019-01-13 08:44:12 +08:00
grid, err := layout.Layout(ly, conf)
if err != nil {
2020-03-08 21:50:20 +08:00
stderrLogger.Print(err)
return 1
}
2019-01-20 11:37:31 +08:00
termWidth, termHeight := ui.TerminalDimensions()
2019-02-02 15:22:27 +08:00
if statusbar {
grid.SetRect(0, 0, termWidth, termHeight-1)
} else {
grid.SetRect(0, 0, termWidth, termHeight)
}
2019-01-01 08:55:50 +08:00
help.Resize(termWidth, termHeight)
ui.Render(grid)
2019-02-02 15:22:27 +08:00
if statusbar {
bar.SetRect(0, termHeight-1, termWidth, termHeight)
ui.Render(bar)
}
2018-12-11 13:21:40 +08:00
if conf.ExportPort != "" {
go func() {
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(conf.ExportPort, nil)
}()
}
eventLoop(conf, grid)
return 0
2018-02-19 15:25:02 +08:00
}
func getLayout(conf gotop.Config) (io.Reader, error) {
switch conf.Layout {
case "-":
return os.Stdin, nil
case "default":
return strings.NewReader(defaultUI), nil
case "minimal":
return strings.NewReader(minimalUI), nil
case "battery":
return strings.NewReader(batteryUI), nil
2020-02-29 00:03:41 +08:00
case "procs":
return strings.NewReader(procsUI), nil
2020-03-07 19:01:47 +08:00
case "kitchensink":
return strings.NewReader(kitchensink), nil
default:
folder := conf.ConfigDir.QueryFolderContainsFile(conf.Layout)
if folder == nil {
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.Existing) {
paths = append(paths, d.Path)
}
return nil, fmt.Errorf("unable find layout file %s in %s", conf.Layout, strings.Join(paths, ", "))
}
lo, err := folder.ReadFile(conf.Layout)
if err != nil {
return nil, err
}
return strings.NewReader(string(lo)), nil
}
}
2020-02-29 00:03:41 +08:00
func loadExtensions(conf gotop.Config) error {
2020-02-29 00:03:41 +08:00
var hasError bool
for _, ex := range conf.Extensions {
exf := ex + ".so"
fn := exf
folder := conf.ConfigDir.QueryFolderContainsFile(fn)
if folder == nil {
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.Existing) {
paths = append(paths, d.Path)
2020-02-29 00:03:41 +08:00
}
log.Printf("unable find extension %s in %s", fn, strings.Join(paths, ", "))
2020-03-08 21:50:20 +08:00
hasError = true
continue
2020-02-29 00:03:41 +08:00
}
fp := filepath.Join(folder.Path, fn)
p, err := plugin.Open(fp)
2020-02-29 00:03:41 +08:00
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 {
2020-03-08 21:50:20 +08:00
return fmt.Errorf("error initializing plugins")
2020-02-29 00:03:41 +08:00
}
return nil
2020-02-29 00:03:41 +08:00
}
func runTests(conf gotop.Config) int {
fmt.Printf("PASS")
return 0
}