Files
service/config/config.go
T
2014-08-17 17:05:42 -07:00

171 lines
3.7 KiB
Go

// A simple and consistent method to extract a configuration from a file.
// This doesn't contain any method to actually decode the file. The actual
// decoding is done in an external function.
package config
import (
"encoding/json"
"io"
"os"
"path/filepath"
"time"
"bitbucket.org/kardianos/osext"
"gopkg.in/fsnotify.v1"
)
const DefaultPostfix = "_config.json"
// Simple JSON based configuration decoder.
func DecodeJsonConfig(r io.Reader, v interface{}) error {
d := json.NewDecoder(r)
return d.Decode(v)
}
// Simple JSON based configuration encoder.
func EncodeJsonConfig(w io.Writer, v interface{}) error {
bb, err := json.MarshalIndent(v, "", "\t")
if err != nil {
return err
}
n, tot := 0, 0
for {
n, err = w.Write(bb[tot:])
tot += n
if len(bb) == tot {
break
}
}
return err
}
// Return a configuration file path. If baseName is empty,
// then the current executable name without the extension
// is used. If postfix is empty then DefaultPostfix is used.
func GetConfigFilePath(baseName, postfix string) (string, error) {
if len(postfix) == 0 {
postfix = DefaultPostfix
}
path, err := osext.Executable()
if err != nil {
return "", err
}
path, exeName := filepath.Split(path)
if len(baseName) == 0 {
exeName = exeName[:len(exeName)-len(filepath.Ext(exeName))]
} else {
exeName = baseName
}
configPath := filepath.Join(path, exeName+postfix)
return configPath, nil
}
type DecodeConfig func(r io.Reader, v interface{}) error
type EncodeConfig func(w io.Writer, v interface{}) error
type WatchConfig struct {
// Notified here if the file changes.
C chan *WatchConfig
filepath string
watch *fsnotify.Watcher
decode DecodeConfig
close chan struct{}
}
// Create a new configuration watcher. Adds a notification if the configuration file changes
// so it may be reloaded. If defaultConfig is not nil and encode is not nil, the
// configuration file path is checked if a file exists or not. If it doesn't exist
// the default configuration is written to the file.
func NewWatchConfig(filepath string, decode DecodeConfig, defaultConfig interface{}, encode EncodeConfig) (*WatchConfig, error) {
if defaultConfig != nil && encode != nil {
f, err := os.Open(filepath)
if f != nil {
f.Close()
}
if os.IsNotExist(err) {
f, err = os.Create(filepath)
if err != nil {
return nil, err
}
err = encode(f, defaultConfig)
f.Close()
if err != nil {
return nil, err
}
}
}
watch, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
err = watch.Add(filepath)
if err != nil {
return nil, err
}
wc := &WatchConfig{
C: make(chan *WatchConfig),
filepath: filepath,
watch: watch,
decode: decode,
close: make(chan struct{}, 0),
}
go wc.run()
return wc, nil
}
func (wc *WatchConfig) run() {
// Work around watch events being sent more then once.
ticker := time.NewTicker(time.Second)
trigger := false
waitOnce := false
for {
select {
case <-wc.close:
close(wc.C)
return
case <-wc.watch.Errors:
// Nothing right now.
case <-wc.watch.Events:
trigger = true
case <-ticker.C:
// Think of this as a PLC state machine.
if trigger && waitOnce {
wc.C <- wc
trigger = false
waitOnce = false
}
if trigger && !waitOnce {
waitOnce = true
}
}
}
}
// Send a notification as if the configuration file has changed.
func (wc *WatchConfig) TriggerC() {
wc.C <- wc
}
// Load the configuration from the file into the provided value.
func (wc *WatchConfig) Load(v interface{}) error {
f, err := os.Open(wc.filepath)
if err != nil {
return err
}
defer f.Close()
return wc.decode(f, v)
}
// Stop the watch and any loops that are running.
func (wc *WatchConfig) Close() error {
wc.close <- struct{}{}
return wc.watch.Close()
}