116 lines
2.9 KiB
Go
116 lines
2.9 KiB
Go
|
package config
|
||
|
|
||
|
import (
|
||
|
"github.com/golang/protobuf/jsonpb"
|
||
|
"github.com/golang/protobuf/proto"
|
||
|
"github.com/ricochet-im/ricochet-go/rpc"
|
||
|
"log"
|
||
|
"os"
|
||
|
"sync"
|
||
|
"sync/atomic"
|
||
|
)
|
||
|
|
||
|
type ConfigFile struct {
|
||
|
filePath string
|
||
|
root *ricochet.Config
|
||
|
readSnapshot atomic.Value
|
||
|
mutex sync.Mutex
|
||
|
}
|
||
|
|
||
|
func NewConfigFile(path string) (*ConfigFile, error) {
|
||
|
cfg := &ConfigFile{
|
||
|
filePath: path,
|
||
|
root: &ricochet.Config{},
|
||
|
}
|
||
|
cfg.readSnapshot.Store(cfg.root)
|
||
|
if err := cfg.save(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return cfg, nil
|
||
|
}
|
||
|
|
||
|
func LoadConfigFile(path string) (*ConfigFile, error) {
|
||
|
cfg := &ConfigFile{
|
||
|
filePath: path,
|
||
|
root: &ricochet.Config{},
|
||
|
}
|
||
|
|
||
|
file, err := os.Open(cfg.filePath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer file.Close()
|
||
|
|
||
|
json := jsonpb.Unmarshaler{
|
||
|
AllowUnknownFields: true,
|
||
|
}
|
||
|
if err := json.Unmarshal(file, cfg.root); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
cfg.readSnapshot.Store(cfg.root)
|
||
|
return cfg, nil
|
||
|
}
|
||
|
|
||
|
// Read returns a **read-only** snapshot of the current configuration. This
|
||
|
// function is threadsafe, and the values in this instance of the configuration
|
||
|
// will not change when the configuration changes.
|
||
|
//
|
||
|
// Do not under any circumstances modify any part of this object.
|
||
|
func (cfg *ConfigFile) Read() *ricochet.Config {
|
||
|
return cfg.readSnapshot.Load().(*ricochet.Config)
|
||
|
}
|
||
|
|
||
|
// Lock gains exclusive control of the configuration state, allowing the caller
|
||
|
// to safely make changes to the configuration. Concurrent readers will not see
|
||
|
// any changes until Unlock() is called, at which point they will atomically be
|
||
|
// visible in future calls to Read() as well as being saved persistently.
|
||
|
func (cfg *ConfigFile) Lock() *ricochet.Config {
|
||
|
cfg.mutex.Lock()
|
||
|
// Clone the current configuration for a mutable copy
|
||
|
cfg.root = proto.Clone(cfg.root).(*ricochet.Config)
|
||
|
return cfg.root
|
||
|
}
|
||
|
|
||
|
func (cfg *ConfigFile) Unlock() {
|
||
|
// Clone cfg.root again to guarantee that any messages are detached from
|
||
|
// instances that exist elsewhere in the code. Inefficient but safe.
|
||
|
cfg.root = proto.Clone(cfg.root).(*ricochet.Config)
|
||
|
cfg.readSnapshot.Store(cfg.root)
|
||
|
err := cfg.save()
|
||
|
if err != nil {
|
||
|
log.Printf("WARNING: Unable to save configuration: %s", err)
|
||
|
}
|
||
|
cfg.mutex.Unlock()
|
||
|
}
|
||
|
|
||
|
func (cfg *ConfigFile) save() error {
|
||
|
json := jsonpb.Marshaler{Indent: " "}
|
||
|
|
||
|
// Make a pathetic attempt at atomic file write by writing into a
|
||
|
// temporary file and renaming over the original; this is probably
|
||
|
// imperfect as-implemented, but better than truncating and writing
|
||
|
// directly.
|
||
|
tempPath := cfg.filePath + ".new"
|
||
|
file, err := os.OpenFile(tempPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||
|
if err != nil {
|
||
|
log.Printf("Config save error: %v", err)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = json.Marshal(file, cfg.root)
|
||
|
if err != nil {
|
||
|
log.Printf("Config encoding error: %v", err)
|
||
|
file.Close()
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
file.Close()
|
||
|
if err := os.Rename(tempPath, cfg.filePath); err != nil {
|
||
|
log.Printf("Config replace error: %v", err)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|