2018-02-19 15:25:02 +08:00
package main
import (
2018-12-11 13:21:40 +08:00
"fmt"
2020-02-18 01:40:16 +08:00
"io"
2018-12-05 13:44:25 +08:00
"log"
2020-02-18 23:44:29 +08:00
"net/http"
2018-02-19 15:25:02 +08:00
"os"
"os/signal"
2018-12-05 13:44:25 +08:00
"path/filepath"
2020-02-29 00:03:41 +08:00
"plugin"
2018-03-09 16:27:46 +08:00
"strconv"
2020-02-13 23:40:20 +08:00
"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"
2020-02-18 23:44:29 +08:00
"github.com/prometheus/client_golang/prometheus/promhttp"
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"
"github.com/xxxserxxx/gotop/v3/utils"
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
2020-02-13 23:40:20 +08:00
defaultUI = "cpu\ndisk/1 2:mem/2\ntemp\nnet procs"
2020-02-14 03:24:41 +08:00
minimalUI = "cpu\nmem procs"
2020-02-16 06:10:45 +08:00
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"
2019-01-13 08:31:37 +08:00
)
2018-02-19 15:25:02 +08:00
var (
2020-03-03 19:47:52 +08:00
// TODO: Set this at compile time; having to check this in sucks.
Version = "3.4.5"
2020-02-14 00:15:52 +08:00
conf gotop . Config
2020-02-13 10:15:49 +08:00
help * w . HelpMenu
bar * w . StatusBar
statusbar bool
2018-12-05 13:44:25 +08:00
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
2020-03-03 03:41:55 +08:00
// 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.
2020-02-18 01:40:16 +08:00
func parseArgs ( conf * gotop . Config ) error {
2018-02-19 15:25:02 +08:00
usage := `
Usage : gotop [ options ]
Options :
2020-02-13 23:40:20 +08:00
- c , -- color = NAME Set a colorscheme .
- h , -- help Show this screen .
2020-02-16 06:10:45 +08:00
- m , -- minimal Only show CPU , Mem and Process widgets . Overrides - l . ( DEPRECATED , use - l minimal )
2020-02-13 23:40:20 +08:00
- 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 .
2020-02-16 06:10:45 +08:00
- b , -- battery Show battery level widget ( ' minimal ' turns off ) . ( DEPRECATED , use - l battery )
2020-02-14 03:24:41 +08:00
- 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 !
2020-02-18 23:44:29 +08:00
- 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 .
2020-03-02 23:09:17 +08:00
-- test Runs tests and exits with success / failure code
2020-02-14 21:52:33 +08:00
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
2019-11-16 08:23:32 +08:00
solarized16 - dark
solarized16 - light
2018-02-22 11:54:25 +08:00
monokai
2019-02-21 15:51:49 +08:00
vice
2018-02-19 15:25:02 +08:00
`
2020-02-18 01:40:16 +08:00
var err error
conf . Colorscheme , err = colorschemes . FromName ( conf . ConfigDir , "default" )
if err != nil {
return err
2020-02-13 10:15:49 +08:00
}
2020-03-03 19:47:52 +08:00
args , err := docopt . ParseArgs ( usage , os . Args [ 1 : ] , Version )
2018-12-05 13:44:25 +08:00
if err != nil {
2020-02-18 01:40:16 +08:00
return err
2018-12-05 13:44:25 +08:00
}
2018-02-21 18:24:36 +08:00
2020-02-13 23:40:20 +08:00
if val , _ := args [ "--layout" ] ; val != nil {
2020-02-18 01:40:16 +08:00
conf . Layout = val . ( string )
2020-02-13 23:40:20 +08:00
}
2018-02-21 18:24:36 +08:00
if val , _ := args [ "--color" ] ; val != nil {
2020-02-18 01:40:16 +08:00
cs , err := colorschemes . FromName ( conf . ConfigDir , val . ( string ) )
2020-02-13 10:15:49 +08:00
if err != nil {
2020-02-18 01:40:16 +08:00
return err
2018-12-11 13:21:40 +08:00
}
2020-02-13 10:15:49 +08:00
conf . Colorscheme = cs
2018-02-21 18:24:36 +08:00
}
2020-02-18 01:40:16 +08:00
if args [ "--averagecpu" ] . ( bool ) {
conf . AverageLoad , _ = args [ "--averagecpu" ] . ( bool )
2020-02-16 06:10:45 +08:00
}
2020-02-18 01:40:16 +08:00
if args [ "--percpu" ] . ( bool ) {
conf . PercpuLoad , _ = args [ "--percpu" ] . ( bool )
2020-02-14 03:24:41 +08:00
}
2020-02-18 01:40:16 +08:00
if args [ "--statusbar" ] . ( bool ) {
statusbar , _ = args [ "--statusbar" ] . ( bool )
2018-12-05 13:44:25 +08:00
}
2020-02-18 01:40:16 +08:00
if args [ "--battery" ] . ( bool ) {
conf . Layout = "battery"
2018-03-09 16:34:26 +08:00
}
2020-02-18 01:40:16 +08:00
if args [ "--minimal" ] . ( bool ) {
conf . Layout = "minimal"
2019-03-01 08:29:52 +08:00
}
2020-02-18 23:44:29 +08:00
if val , _ := args [ "--export" ] ; val != nil {
conf . ExportPort = val . ( string )
}
if val , _ := args [ "--rate" ] ; val != nil {
rateStr , _ := val . ( string )
2020-02-18 01:40:16 +08:00
rate , err := strconv . ParseFloat ( rateStr , 64 )
2019-01-15 11:39:37 +08:00
if err != nil {
2020-02-18 01:40:16 +08:00
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
}
2018-09-19 04:42:49 +08:00
}
2020-02-18 01:40:16 +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
}
2020-02-18 01:40:16 +08:00
if val , _ := args [ "--interface" ] ; val != nil {
conf . NetInterface , _ = args [ "--interface" ] . ( string )
2018-09-19 04:42:49 +08:00
}
2020-02-29 00:03:41 +08:00
if val , _ := args [ "--extensions" ] ; val != nil {
exs , _ := args [ "--extensions" ] . ( string )
conf . Extensions = strings . Split ( exs , "," )
}
2020-03-02 23:09:17 +08:00
if val , _ := args [ "--test" ] ; val != nil {
conf . Test = val . ( bool )
}
2020-02-18 01:40:16 +08:00
return nil
2018-02-19 15:25:02 +08:00
}
2020-02-14 00:15:52 +08:00
func setDefaultTermuiColors ( c gotop . Config ) {
2020-02-13 10:15:49 +08:00
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 ) {
2020-02-13 10:15:49 +08:00
drawTicker := time . NewTicker ( c . UpdateInterval ) . C
2018-02-19 15:25:02 +08:00
2018-11-30 10:17:13 +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
2018-11-30 10:17:13 +08:00
uiEvents := ui . PollEvents ( )
2018-02-21 19:41:40 +08:00
2018-11-30 10:17:13 +08:00
previousKey := ""
2018-02-19 15:25:02 +08:00
2018-11-30 10:17:13 +08:00
for {
select {
case <- sigTerm :
return
case <- drawTicker :
2020-02-13 10:15:49 +08:00
if ! c . HelpVisible {
2019-03-08 15:43:10 +08:00
ui . Render ( grid )
2019-03-17 22:54:56 +08:00
if statusbar {
ui . Render ( bar )
}
2018-11-30 10:17:13 +08:00
}
case e := <- uiEvents :
2020-02-14 23:35:58 +08:00
if grid . Proc != nil && grid . Proc . HandleEvent ( e ) {
ui . Render ( grid . Proc )
break
}
2018-11-30 10:17:13 +08:00
switch e . ID {
case "q" , "<C-c>" :
return
case "?" :
2020-02-13 10:15:49 +08:00
c . HelpVisible = ! c . HelpVisible
2019-01-08 12:10:09 +08:00
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 )
}
2019-01-08 12:10:09 +08:00
help . Resize ( payload . Width , payload . Height )
ui . Clear ( )
}
2020-02-13 10:15:49 +08:00
if c . HelpVisible {
2019-01-08 12:10:09 +08:00
switch e . ID {
case "?" :
2018-02-19 15:25:02 +08:00
ui . Clear ( )
ui . Render ( help )
2019-01-08 12:10:09 +08:00
case "<Escape>" :
2020-02-13 10:15:49 +08:00
c . HelpVisible = false
2019-03-08 15:43:10 +08:00
ui . Render ( grid )
2019-01-08 12:10:09 +08:00
case "<Resize>" :
ui . Render ( help )
2018-02-19 15:25:02 +08:00
}
2019-01-08 12:10:09 +08:00
} else {
switch e . ID {
case "?" :
2019-03-08 15:43:10 +08:00
ui . Render ( grid )
2019-01-08 12:10:09 +08:00
case "h" :
2020-02-13 10:15:49 +08:00
c . GraphHorizontalScale += graphHorizontalScaleDelta
for _ , item := range grid . Lines {
item . Scale ( c . GraphHorizontalScale )
}
ui . Render ( grid )
2019-01-08 12:10:09 +08:00
case "l" :
2020-02-13 10:15:49 +08:00
if c . GraphHorizontalScale > graphHorizontalScaleDelta {
c . GraphHorizontalScale -= graphHorizontalScaleDelta
for _ , item := range grid . Lines {
item . Scale ( c . GraphHorizontalScale )
ui . Render ( item )
}
2018-11-30 10:17:13 +08:00
}
2019-01-08 12:10:09 +08:00
case "<Resize>" :
2019-03-08 15:43:10 +08:00
ui . Render ( grid )
2019-02-06 18:11:41 +08:00
if statusbar {
2019-03-08 15:43:10 +08:00
ui . Render ( bar )
2019-02-06 18:11:41 +08:00
}
2019-01-08 12:10:09 +08:00
case "<MouseLeft>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
payload := e . Payload . ( ui . Mouse )
grid . Proc . HandleClick ( payload . X , payload . Y )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "k" , "<Up>" , "<MouseWheelUp>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollUp ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "j" , "<Down>" , "<MouseWheelDown>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollDown ( )
ui . Render ( grid . Proc )
}
2019-02-02 15:22:27 +08:00
case "<Home>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollTop ( )
ui . Render ( grid . Proc )
}
2019-02-02 15:06:57 +08:00
case "g" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
if previousKey == "g" {
grid . Proc . ScrollTop ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
}
case "G" , "<End>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollBottom ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "<C-d>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollHalfPageDown ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "<C-u>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollHalfPageUp ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "<C-f>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollPageDown ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "<C-b>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ScrollPageUp ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "d" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
if previousKey == "d" {
2020-02-14 03:38:45 +08:00
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" )
2020-02-13 10:15:49 +08:00
}
2019-01-08 12:10:09 +08:00
}
case "<Tab>" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ToggleShowingGroupedProcs ( )
ui . Render ( grid . Proc )
}
2019-01-08 12:10:09 +08:00
case "m" , "c" , "p" :
2020-02-13 10:15:49 +08:00
if grid . Proc != nil {
grid . Proc . ChangeProcSortMethod ( w . ProcSortMethod ( e . ID ) )
ui . Render ( grid . Proc )
}
2020-02-14 23:35:58 +08:00
case "/" :
if grid . Proc != nil {
grid . Proc . SetEditingFilter ( true )
ui . Render ( grid . Proc )
}
2018-11-30 10:17:13 +08:00
}
2019-01-08 12:10:09 +08:00
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-11-30 10:17:13 +08:00
}
}
2018-02-19 15:25:02 +08:00
2020-02-18 01:40:16 +08:00
func makeConfig ( ) gotop . Config {
ld := utils . GetLogDir ( appName )
cd := utils . GetConfigDir ( appName )
conf = gotop . Config {
ConfigDir : cd ,
LogDir : ld ,
LogFile : "errors.log" ,
GraphHorizontalScale : 7 ,
HelpVisible : false ,
UpdateInterval : time . Second ,
AverageLoad : false ,
2020-02-18 23:44:29 +08:00
PercpuLoad : true ,
2020-02-18 01:40:16 +08:00
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 ( ) {
2020-02-18 01:40:16 +08:00
// Set up default config
conf := makeConfig ( )
// Parse the config file
cfn := filepath . Join ( conf . ConfigDir , "gotop.conf" )
if cf , err := os . Open ( cfn ) ; err == nil {
err := gotop . Parse ( cf , & conf )
if err != nil {
stderrLogger . Fatalf ( "error parsing config file %v" , err )
}
}
// Override with command line arguments
err := parseArgs ( & conf )
2020-02-13 10:15:49 +08:00
if err != nil {
2018-12-11 13:21:40 +08:00
stderrLogger . Fatalf ( "failed to parse cli args: %v" , err )
}
2020-02-16 04:27:31 +08:00
logfile , err := logging . New ( conf )
2019-01-20 11:37:31 +08:00
if err != nil {
stderrLogger . Fatalf ( "failed to setup log file: %v" , err )
}
2019-02-07 09:18:44 +08:00
defer logfile . Close ( )
2019-01-20 11:37:31 +08:00
2020-03-02 23:09:17 +08:00
lstream := getLayout ( conf )
ly := layout . ParseLayout ( lstream )
loadExtensions ( conf )
if conf . Test {
os . Exit ( runTests ( conf ) )
}
2018-12-10 13:19:09 +08:00
if err := ui . Init ( ) ; err != nil {
2018-12-11 13:21:40 +08:00
stderrLogger . Fatalf ( "failed to initialize termui: %v" , err )
2018-11-30 10:17:13 +08:00
}
defer ui . Close ( )
2018-12-11 13:21:40 +08:00
2020-02-13 10:15:49 +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
2020-02-13 10:15:49 +08:00
grid , err := layout . Layout ( ly , conf )
if err != nil {
stderrLogger . Fatalf ( "failed to initialize termui: %v" , err )
}
2019-01-20 11:37:31 +08:00
2019-01-24 13:23:35 +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
2020-02-18 23:44:29 +08:00
if conf . ExportPort != "" {
go func ( ) {
http . Handle ( "/metrics" , promhttp . Handler ( ) )
http . ListenAndServe ( conf . ExportPort , nil )
} ( )
}
2020-02-13 10:15:49 +08:00
eventLoop ( conf , grid )
2018-02-19 15:25:02 +08:00
}
2020-02-18 01:40:16 +08:00
func getLayout ( conf gotop . Config ) io . Reader {
switch conf . Layout {
case "-" :
return os . Stdin
case "default" :
return strings . NewReader ( defaultUI )
case "minimal" :
return strings . NewReader ( minimalUI )
case "battery" :
return strings . NewReader ( batteryUI )
2020-02-29 00:03:41 +08:00
case "procs" :
return strings . NewReader ( procsUI )
2020-02-18 01:40:16 +08:00
default :
fp := filepath . Join ( conf . ConfigDir , conf . Layout )
fin , err := os . Open ( fp )
if err != nil {
fin , err = os . Open ( conf . Layout )
if err != nil {
log . Fatalf ( "Unable to open layout file %s or ./%s" , fp , conf . Layout )
}
}
return fin
}
}
2020-02-29 00:03:41 +08:00
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 )
}
}
2020-03-02 23:09:17 +08:00
func runTests ( conf gotop . Config ) int {
fmt . Printf ( "PASS" )
return 0
}