core: Basic thread-safe and writable config API
This is ugly API for now, but it's a simple and relatively safe solution. It should be cleaned up later. Data from the Config object can only be accessed by opening the "root" for reading (OpenRead) or writing (OpenWrite). Multiple readers may be open simultaneously, but only one writer, which guarantees atomic behavior. There are ugly edge-cases for save errors and pointer-style objects in the config tree, so use good behavior.
This commit is contained in:
parent
be5a8f1f4e
commit
ca3c672975
129
core/config.go
129
core/config.go
|
@ -4,15 +4,26 @@ import (
|
|||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
path string
|
||||
filePath string
|
||||
|
||||
root ConfigRoot
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type ConfigRoot struct {
|
||||
Contacts map[string]ConfigContact
|
||||
Identity ConfigIdentity
|
||||
|
||||
// Not used by the permanent instance in Config
|
||||
writable bool
|
||||
config *Config
|
||||
}
|
||||
|
||||
type ConfigContact struct {
|
||||
|
@ -29,20 +40,122 @@ type ConfigIdentity struct {
|
|||
}
|
||||
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
configFile := filepath.Join(configPath, "ricochet.json")
|
||||
configData, err := ioutil.ReadFile(configFile)
|
||||
config := &Config{
|
||||
path: configPath,
|
||||
filePath: filepath.Join(configPath, "notricochet.json"),
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(config.filePath)
|
||||
if err != nil {
|
||||
log.Printf("Config read error from %s: %v", configFile, err)
|
||||
log.Printf("Config read error from %s: %v", config.filePath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var root ConfigRoot
|
||||
if err := json.Unmarshal(configData, &root); err != nil {
|
||||
if err := json.Unmarshal(data, &config.root); err != nil {
|
||||
log.Printf("Config parse error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Config: %v", root)
|
||||
|
||||
return &Config{}, nil
|
||||
log.Printf("Config: %v", config.root)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Return a read-only snapshot of the current configuration. This object
|
||||
// _must not_ be stored, and you must call Close() when finished with it.
|
||||
// This function may block.
|
||||
func (c *Config) OpenRead() *ConfigRoot {
|
||||
c.mutex.RLock()
|
||||
root := c.root
|
||||
root.writable = false
|
||||
root.config = c
|
||||
return &root
|
||||
}
|
||||
|
||||
// Return a writable snapshot of the current configuration. This object
|
||||
// _must not_ be stored, and you must call Save() or Discard() when
|
||||
// finished with it. This function may block.
|
||||
func (c *Config) OpenWrite() *ConfigRoot {
|
||||
c.mutex.Lock()
|
||||
root := c.root
|
||||
root.writable = true
|
||||
root.config = c
|
||||
return &root
|
||||
}
|
||||
|
||||
func (root *ConfigRoot) Close() {
|
||||
if root.writable {
|
||||
log.Panic("Close called on writable config object; use Save or Discard")
|
||||
}
|
||||
if root.config == nil {
|
||||
log.Panic("Close called on closed config object")
|
||||
}
|
||||
root.config.mutex.RUnlock()
|
||||
root.config = nil
|
||||
}
|
||||
|
||||
// Save saves the state to the Config object, and attempts to write to
|
||||
// disk. An error is returned if the write fails, but changes to the
|
||||
// object are not discarded on error. XXX This is bad API
|
||||
func (root *ConfigRoot) Save() error {
|
||||
if !root.writable {
|
||||
log.Panic("Save called on read-only config object")
|
||||
}
|
||||
if root.config == nil {
|
||||
log.Panic("Save called on closed config object")
|
||||
}
|
||||
c := root.config
|
||||
root.writable = false
|
||||
root.config = nil
|
||||
c.root = *root
|
||||
err := c.save()
|
||||
c.mutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Discard cannot be relied on to restore the state exactly as it was,
|
||||
// because of potentially shared slice or map objects, but does not do
|
||||
// an immediate save to disk. XXX This is bad API
|
||||
func (root *ConfigRoot) Discard() {
|
||||
if !root.writable {
|
||||
log.Panic("Discard called on read-only config object")
|
||||
}
|
||||
if root.config == nil {
|
||||
log.Panic("Discard called on closed config object")
|
||||
}
|
||||
c := root.config
|
||||
root.config = nil
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (c *Config) save() error {
|
||||
data, err := json.MarshalIndent(c.root, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("Config encoding error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 := c.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
|
||||
}
|
||||
|
||||
if _, err := file.Write(data); err != nil {
|
||||
log.Printf("Config write error: %v", err)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
if err := os.Rename(tempPath, c.filePath); err != nil {
|
||||
log.Printf("Config replace error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue