From ca3c6729759b8f148a75e71a0ad3fc790bf8a41a Mon Sep 17 00:00:00 2001 From: John Brooks Date: Sun, 28 Aug 2016 21:36:42 -0600 Subject: [PATCH] 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. --- core/config.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/core/config.go b/core/config.go index 63e13a8..b62aa9b 100644 --- a/core/config.go +++ b/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 }