1
0
Fork 0
secondly/secondly.go

243 lines
5.4 KiB
Go
Raw Permalink Normal View History

2015-08-29 19:36:37 +00:00
// Package secondly is a configuration management plugin for Go projects.
// It takes care of the app's configuration, specifically of updating it in
// runtime. It is capable of listening to file system events and signals to
// trigger configuration reload. It also provides a nice web GUI editor.
2015-08-29 12:41:20 +00:00
package secondly
2015-08-29 09:45:00 +00:00
import (
2015-08-29 16:50:22 +00:00
"bytes"
2015-08-29 09:45:00 +00:00
"encoding/json"
2015-08-29 10:51:28 +00:00
"flag"
2015-08-29 13:56:19 +00:00
"fmt"
2015-08-29 09:59:45 +00:00
"log"
2015-08-29 11:14:53 +00:00
"os"
"os/signal"
"path/filepath"
2015-08-29 09:45:00 +00:00
"reflect"
"strings"
2015-08-29 11:14:53 +00:00
"syscall"
"github.com/howeyc/fsnotify"
2015-08-29 09:45:00 +00:00
)
var (
2015-08-29 10:51:28 +00:00
config interface{} // config stores application config
configFile string
callbacks = make(map[string][]func(oldVal, newVal interface{}))
initialized bool
2015-08-29 17:36:27 +00:00
initFunc func()
2015-08-29 09:45:00 +00:00
)
2015-08-29 19:36:37 +00:00
// SetupFlags sets up Secondly's configuration flags.
func SetupFlags() {
2015-08-29 14:32:39 +00:00
if flag.Parsed() {
log.Fatalln("secondly.SetupFlags() must be called before flag.Parse()")
}
2015-08-29 10:51:28 +00:00
flag.StringVar(&configFile, "config", "config.json", "Path to config file")
}
2015-08-29 09:55:34 +00:00
// Manage accepts a pointer to a configuration struct.
func Manage(target interface{}) {
if ok := isStructPtr(target); !ok {
panic("Argument must be a pointer to a struct")
}
2015-08-29 18:51:12 +00:00
assign(target)
2015-08-29 11:10:56 +00:00
bootstrap()
2015-08-29 09:55:34 +00:00
}
2015-08-29 13:56:19 +00:00
// StartServer will start an HTTP server with web interface to edit config.
func StartServer(host string, port int) {
go startServer(fmt.Sprintf("%s:%d", host, port))
}
2015-08-29 11:14:53 +00:00
// HandleSIGHUP waits a SIGHUP system call and reloads configuration when
// receives one.
func HandleSIGHUP() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP)
go func() {
2015-08-29 19:36:37 +00:00
for range ch {
2015-08-29 11:14:53 +00:00
log.Println("SIGHUP received, reloading config")
readConfig()
}
}()
}
2015-08-29 19:36:37 +00:00
// HandleFileSystemEvents listens to file system events and reloads configuration when
// config file is modified.
2015-08-29 19:36:37 +00:00
func HandleFileSystemEvents() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
}
if err := watcher.WatchFlags(filepath.Dir(configFile), fsnotify.FSN_MODIFY); err != nil {
panic(err)
}
fname := configFile
if ss := strings.Split(configFile, "/"); len(ss) > 1 {
fname = ss[len(ss)-1]
}
go func() {
for {
select {
case e := <-watcher.Event:
if e.Name != fname {
continue
}
log.Println("Config file was modified, reloading")
readConfig()
case err := <-watcher.Error:
log.Println("fsnotify error:", err)
}
}
}()
}
2015-08-29 17:36:27 +00:00
// OnLoad sets up a callback function that would be called once configuration
// is loaded for the first time.
func OnLoad(fun func()) {
initFunc = fun
}
2015-08-29 10:18:32 +00:00
// OnChange adds a callback function that is triggered every time a value of
2015-08-29 19:36:37 +00:00
// a field changes. Field must be a json tag of the struct field.
2015-08-29 10:18:32 +00:00
func OnChange(field string, fun func(oldVal, newVal interface{})) {
callbacks[field] = append(callbacks[field], fun)
}
2015-08-29 19:36:37 +00:00
// asign is responsible for assigning new config value. It is complicated
// because we're changing the value of an interface which is defined in
// another package.
2015-08-29 18:51:12 +00:00
func assign(target interface{}) {
if config == nil {
config = target
return
}
cval := reflect.ValueOf(config).Elem()
tval := reflect.ValueOf(target).Elem()
cval.Set(tval)
}
2015-08-29 19:36:37 +00:00
// bootstrap sets up initial configuration.
2015-08-29 10:51:28 +00:00
func bootstrap() {
if configFile == "" {
2015-08-29 14:32:39 +00:00
log.Fatalln("path to config file is not set")
2015-08-29 10:51:28 +00:00
}
if fileExist(configFile) {
log.Println("Loading config file")
readConfig()
2015-08-29 10:51:28 +00:00
} else {
2015-08-29 14:32:39 +00:00
log.Fatalln("Config file not found")
}
}
func readConfig() {
body, err := readFile(configFile)
if err != nil {
panic(err)
}
updateConfig(body)
}
func writeConfig() {
2015-08-29 16:50:22 +00:00
if err := writeFile(configFile, marshal(config)); err != nil {
panic(err)
2015-08-29 10:51:28 +00:00
}
}
func updateConfig(body []byte) {
2015-08-29 18:51:12 +00:00
// Making a copy of old config for further comparison
old := duplicate(config)
// Making a second copy that we will fill with new data
2015-08-29 09:59:45 +00:00
dupe := duplicate(config)
2015-08-29 18:51:12 +00:00
if err := json.Unmarshal(body, dupe); err != nil {
2015-08-29 14:32:39 +00:00
panic("Failed to update config")
2015-08-29 09:59:45 +00:00
return
}
2015-08-29 10:18:32 +00:00
// Setting new config
2015-08-29 18:51:12 +00:00
assign(dupe)
triggerCallbacks(old, dupe)
2015-08-29 09:59:45 +00:00
}
2015-08-29 16:50:22 +00:00
func marshal(obj interface{}) []byte {
body, err := json.Marshal(config)
if err != nil {
panic(err)
}
out := bytes.NewBuffer([]byte{})
// Indent with empty prefix and four spaces
if err = json.Indent(out, body, "", " "); err != nil {
panic(err)
}
// Adding a trailing newline
// It's good for your carma
out.WriteByte('\n')
return out.Bytes()
}
2015-08-29 10:18:32 +00:00
func triggerCallbacks(oldConf, newConf interface{}) {
2015-08-29 10:51:28 +00:00
// Don't trigger callbacks on fist load
if !initialized {
initialized = true
2015-08-29 17:44:48 +00:00
if initFunc != nil {
initFunc()
}
2015-08-29 10:51:28 +00:00
return
}
if len(callbacks) == 0 {
return
}
2015-08-29 12:29:56 +00:00
for fname, d := range diff(oldConf, newConf) {
if cbs, ok := callbacks[fname]; ok {
for _, cb := range cbs {
cb(d[0], d[1])
}
}
}
2015-08-29 10:18:32 +00:00
return
}
2015-08-29 19:36:37 +00:00
// Secondly accepts only a pointer to stuct as its config value. Here we're
// making sure the right argument is provided.
2015-08-29 09:45:00 +00:00
func isStructPtr(target interface{}) bool {
if val := reflect.ValueOf(target); val.Kind() == reflect.Ptr {
if val = reflect.Indirect(val); val.Kind() == reflect.Struct {
return true
}
}
return false
}
2015-08-29 19:36:37 +00:00
// duplicate creates a copy of a value behind the config interface. Such copies
// are used to replace config value and to check for changes.
2015-08-29 09:55:18 +00:00
func duplicate(original interface{}) interface{} {
// Get the interface value
val := reflect.ValueOf(original)
// We expect a pointer to a struct, so now we need the underlying staruct
val = reflect.Indirect(val)
// Now we need the type (name) of this struct
typ := val.Type()
// Creating a duplicate instance of that struct
2015-08-29 18:51:12 +00:00
dupe := reflect.New(typ)
// Value copy
dupe.Elem().Set(val)
2015-08-29 09:55:18 +00:00
2015-08-29 18:51:12 +00:00
return dupe.Interface()
2015-08-29 09:55:18 +00:00
}