From 9ebcff9b78ef6c529969d34ebac757af9a28ebb2 Mon Sep 17 00:00:00 2001 From: "Sean E. Russell" Date: Sat, 7 Mar 2020 09:03:25 -0600 Subject: Add ability to look in system-wide directories for configuration files --- CHANGELOG.md | 14 +++++++ cmd/gotop/main.go | 98 ++++++++++++++++++++++++++++-------------------- colorschemes/registry.go | 22 ++++++++--- config.go | 27 +++++++++---- go.mod | 1 + go.sum | 2 + logging/logging.go | 14 +++++-- 7 files changed, 120 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72af996..4b1d134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > - **Fixed**: for any bug fixes. > - **Security**: in case of vulnerabilities. +## [3.6.0] - ??? + +### Added + +- Adds support for system-wide configurations. This improves support for package maintainers. + +### Changed + +- Log files stored in \$XDG_CACHE_HOME; DATA, CONFIG, CACHE, and RUNTIME are the only directories specified by the FreeDesktop spec. + +### Removed + +- configdir, logdir, and logfile options in the config file are no longer used. gotop looks for a configuration file, layouts, and colorschemes in the following order: command-line; `pwd`; user-home, and finally a system-wide path. The paths depend on the OS and whether XDG is in use. + ## [3.5.1] - ?? ### Fixed diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go index b1dcc4d..c408f18 100644 --- a/cmd/gotop/main.go +++ b/cmd/gotop/main.go @@ -17,12 +17,12 @@ import ( docopt "github.com/docopt/docopt.go" ui "github.com/gizak/termui/v3" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/shibukawa/configdir" "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" ) @@ -73,6 +73,7 @@ Options: -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. --test Runs tests and exits with success/failure code + --print-paths List out the paths that gotop will look for gotop.conf, layouts, color schemes, and extensions Built-in layouts: @@ -158,6 +159,14 @@ Colorschemes: 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) + } return nil } @@ -347,12 +356,10 @@ func eventLoop(c gotop.Config, grid *layout.MyGrid) { } func makeConfig() gotop.Config { - ld := utils.GetLogDir(appName) - cd := utils.GetConfigDir(appName) + cd := configdir.New("", appName) + cd.LocalPath, _ = filepath.Abs(".") conf = gotop.Config{ ConfigDir: cd, - LogDir: ld, - LogFile: "errors.log", GraphHorizontalScale: 7, HelpVisible: false, UpdateInterval: time.Second, @@ -371,27 +378,29 @@ func makeConfig() gotop.Config { func main() { // 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) - } + // Find the config file; look in (1) local, (2) user, (3) global + err := conf.Load() + if err != nil { + stderrLogger.Printf("failed to parse config file: %s", err) } // Override with command line arguments - err := parseArgs(&conf) + err = parseArgs(&conf) if err != nil { stderrLogger.Fatalf("failed to parse cli args: %v", err) } logfile, err := logging.New(conf) if err != nil { - stderrLogger.Fatalf("failed to setup log file: %v", err) + fmt.Printf("failed to setup log file: %v\n", err) + os.Exit(1) } defer logfile.Close() - lstream := getLayout(conf) + lstream, err := getLayout(conf) + if err != nil { + fmt.Printf("failed to find layou: %s\n", err) + os.Exit(1) + } ly := layout.ParseLayout(lstream) loadExtensions(conf) @@ -439,30 +448,34 @@ func main() { eventLoop(conf, grid) } -func getLayout(conf gotop.Config) io.Reader { +func getLayout(conf gotop.Config) (io.Reader, error) { switch conf.Layout { case "-": - return os.Stdin + return os.Stdin, nil case "default": - return strings.NewReader(defaultUI) + return strings.NewReader(defaultUI), nil case "minimal": - return strings.NewReader(minimalUI) + return strings.NewReader(minimalUI), nil case "battery": - return strings.NewReader(batteryUI) + return strings.NewReader(batteryUI), nil case "procs": - return strings.NewReader(procsUI) + return strings.NewReader(procsUI), nil case "kitchensink": - return strings.NewReader(kitchensink) + return strings.NewReader(kitchensink), nil 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) + 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 fin + return strings.NewReader(string(lo)), nil } } @@ -471,18 +484,17 @@ func loadExtensions(conf gotop.Config) { 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 + 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) } + log.Printf("unable find extension %s in %s", fn, strings.Join(paths, ", ")) + continue } - p, err := plugin.Open(fn) + fp := filepath.Join(folder.Path, fn) + p, err := plugin.Open(fp) if err != nil { hasError = true log.Printf(err.Error()) @@ -494,7 +506,6 @@ func loadExtensions(conf gotop.Config) { log.Printf(err.Error()) continue } - initFunc, ok := init.(func()) if !ok { hasError = true @@ -505,7 +516,12 @@ func loadExtensions(conf gotop.Config) { } if hasError { ui.Close() - fmt.Printf("Error initializing requested plugins; check the log file %s\n", filepath.Join(conf.ConfigDir, conf.LogFile)) + folder := conf.ConfigDir.QueryFolderContainsFile(logging.LOGFILE) + if folder == nil { + fmt.Printf("error initializing requested plugins\n") + } else { + fmt.Printf("error initializing requested plugins; check the log file %s\n", filepath.Join(folder.Path, logging.LOGFILE)) + } os.Exit(1) } } diff --git a/colorschemes/registry.go b/colorschemes/registry.go index d79271d..8f86491 100644 --- a/colorschemes/registry.go +++ b/colorschemes/registry.go @@ -3,8 +3,10 @@ package colorschemes import ( "encoding/json" "fmt" - "io/ioutil" "path/filepath" + "strings" + + "github.com/shibukawa/configdir" ) var registry map[string]Colorscheme @@ -15,7 +17,7 @@ func init() { } } -func FromName(confDir string, c string) (Colorscheme, error) { +func FromName(confDir configdir.ConfigDir, c string) (Colorscheme, error) { cs, ok := registry[c] if !ok { cs, err := getCustomColorscheme(confDir, c) @@ -34,12 +36,20 @@ func register(name string, c Colorscheme) { } // getCustomColorscheme tries to read a custom json colorscheme from /.json -func getCustomColorscheme(confDir string, name string) (Colorscheme, error) { +func getCustomColorscheme(confDir configdir.ConfigDir, name string) (Colorscheme, error) { var cs Colorscheme - filePath := filepath.Join(confDir, name+".json") - dat, err := ioutil.ReadFile(filePath) + fn := name + ".json" + folder := confDir.QueryFolderContainsFile(fn) + if folder == nil { + paths := make([]string, 0) + for _, d := range confDir.QueryFolders(configdir.Existing) { + paths = append(paths, d.Path) + } + return cs, fmt.Errorf("failed to find colorscheme file %s in %s", fn, strings.Join(paths, ", ")) + } + dat, err := folder.ReadFile(fn) if err != nil { - return cs, fmt.Errorf("failed to read colorscheme file: %v", err) + return cs, fmt.Errorf("failed to read colorscheme file %s: %v", filepath.Join(folder.Path, fn), err) } err = json.Unmarshal(dat, &cs) if err != nil { diff --git a/config.go b/config.go index d072dee..de543d3 100644 --- a/config.go +++ b/config.go @@ -4,19 +4,20 @@ import ( "bufio" "fmt" "io" + "log" + "os" "strconv" "strings" "time" + "github.com/shibukawa/configdir" "github.com/xxxserxxx/gotop/v3/colorschemes" "github.com/xxxserxxx/gotop/v3/widgets" ) // TODO: test, build, release [#119] [#120] [#121] type Config struct { - ConfigDir string - LogDir string - LogFile string + ConfigDir configdir.ConfigDir GraphHorizontalScale int HelpVisible bool @@ -36,7 +37,19 @@ type Config struct { Test bool } -func Parse(in io.Reader, conf *Config) error { +func (conf *Config) Load() error { + var in io.Reader + cfn := "gotop.conf" + folder := conf.ConfigDir.QueryFolderContainsFile(cfn) + if folder != nil { + if cf, err := os.Open(cfn); err == nil { + defer cf.Close() + } else { + return err + } + } else { + return nil + } r := bufio.NewScanner(in) var lineNo int for r.Scan() { @@ -50,11 +63,11 @@ func Parse(in io.Reader, conf *Config) error { default: return fmt.Errorf("invalid config option %s", key) case "configdir": - conf.ConfigDir = kv[1] + log.Printf("configdir is deprecated. Ignored configdir=%s", kv[1]) case "logdir": - conf.LogDir = kv[1] + log.Printf("logdir is deprecated. Ignored logdir=%s", kv[1]) case "logfile": - conf.LogFile = kv[1] + log.Printf("logfile is deprecated. Ignored logfile=%s", kv[1]) case "graphhorizontalscale": iv, err := strconv.Atoi(kv[1]) if err != nil { diff --git a/go.mod b/go.mod index 38e1380..2b53f3a 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/mattn/go-runewidth v0.0.4 github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/prometheus/client_golang v1.4.1 + github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 github.com/shirou/gopsutil v2.18.11+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum index 3cf7498..0a20cb4 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= +github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y= github.com/shirou/gopsutil v2.18.11+incompatible h1:PMFTKnFTr/YTRW5rbLK4vWALV3a+IGXse5nvhSjztmg= github.com/shirou/gopsutil v2.18.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= diff --git a/logging/logging.go b/logging/logging.go index 6b023d7..c79b5c8 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -11,16 +11,22 @@ import ( "github.com/xxxserxxx/gotop/v3" ) +const ( + LOGFILE = "errors.log" +) + func New(c gotop.Config) (io.WriteCloser, error) { // create the log directory - if err := os.MkdirAll(c.LogDir, 0755); err != nil { - return nil, fmt.Errorf("failed to make the log directory: %v", err) + cache := c.ConfigDir.QueryCacheFolder() + err := cache.MkdirAll() + if err != nil && !os.IsExist(err) { + return nil, err } w := &RotateWriter{ - filename: filepath.Join(c.LogDir, c.LogFile), + filename: filepath.Join(cache.Path, LOGFILE), maxLogSize: c.MaxLogSize, } - err := w.rotate() + err = w.rotate() if err != nil { return nil, err } -- cgit v1.2.3