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:
John Brooks 2016-08-28 21:36:42 -06:00
parent be5a8f1f4e
commit ca3c672975
1 changed files with 121 additions and 8 deletions

View File

@ -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
}