github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/config/instance.go (about) 1 package config 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "os" 7 "path/filepath" 8 "sync" 9 "time" 10 11 C "github.com/ActiveState/cli/internal/constants" 12 "github.com/ActiveState/cli/internal/errs" 13 "github.com/ActiveState/cli/internal/installation/storage" 14 "github.com/ActiveState/cli/internal/logging" 15 mediator "github.com/ActiveState/cli/internal/mediators/config" 16 "github.com/ActiveState/cli/internal/multilog" 17 "github.com/ActiveState/cli/internal/profile" 18 "github.com/ActiveState/cli/internal/rtutils/singlethread" 19 "github.com/spf13/cast" 20 "gopkg.in/yaml.v2" 21 _ "modernc.org/sqlite" 22 ) 23 24 // Instance holds our main config logic 25 type Instance struct { 26 appDataDir string 27 thread *singlethread.Thread 28 closeThread bool 29 db *sql.DB 30 closed bool 31 } 32 33 func New() (*Instance, error) { 34 defer profile.Measure("config.New", time.Now()) 35 return NewCustom("", singlethread.New(), true) 36 } 37 38 // NewCustom is intended only to be used from tests or internally to this package 39 func NewCustom(localPath string, thread *singlethread.Thread, closeThread bool) (*Instance, error) { 40 i := &Instance{} 41 i.thread = thread 42 i.closeThread = closeThread 43 44 var err error 45 if localPath != "" { 46 i.appDataDir, err = storage.AppDataPathWithParent(localPath) 47 } else { 48 i.appDataDir, err = storage.AppDataPath() 49 } 50 if err != nil { 51 return nil, errs.Wrap(err, "Could not detect appdata dir") 52 } 53 54 // Ensure appdata dir exists, because the sqlite driver sure doesn't 55 if _, err := os.Stat(i.appDataDir); os.IsNotExist(err) { 56 err = os.MkdirAll(i.appDataDir, os.ModePerm) 57 if err != nil { 58 return nil, errs.Wrap(err, "Could not create config dir") 59 } 60 } 61 62 path := filepath.Join(i.appDataDir, C.InternalConfigFileName) 63 64 t := time.Now() 65 i.db, err = sql.Open("sqlite", path) 66 if err != nil { 67 return nil, errs.Wrap(err, "Could not create sqlite connection to %s", path) 68 } 69 profile.Measure("config.sqlOpen", t) 70 71 t = time.Now() 72 _, err = i.db.Exec(`CREATE TABLE IF NOT EXISTS config (key string NOT NULL PRIMARY KEY, value text)`) 73 if err != nil { 74 return nil, errs.Wrap(err, "Could not seed settings database") 75 } 76 profile.Measure("config.createTable", t) 77 78 return i, nil 79 } 80 81 func (i *Instance) Close() error { 82 mutex := sync.Mutex{} 83 mutex.Lock() 84 defer mutex.Unlock() 85 86 if i.closed { 87 return nil 88 } 89 i.closed = true 90 if i.closeThread { 91 i.thread.Close() 92 } 93 return i.db.Close() 94 } 95 96 func (i *Instance) Closed() bool { 97 return i.closed 98 } 99 100 // GetThenSet updates a value at the given key. The valueF argument returns the 101 // new value to set based on the previous one. If the function returns with an error, the 102 // update is cancelled. The function ensures that no-other process or thread can modify 103 // the key between reading of the old value and setting the new value. 104 func (i *Instance) GetThenSet(key string, valueF func(currentValue interface{}) (interface{}, error)) error { 105 return i.thread.Run(func() error { 106 return i.setWithCallback(key, valueF) 107 }) 108 } 109 110 const CancelSet = "__CANCEL__" 111 112 func (i *Instance) setWithCallback(key string, valueF func(currentValue interface{}) (interface{}, error)) (rerr error) { 113 defer func() { 114 if rerr != nil { 115 logging.Warning("setWithCallback error: %v", errs.JoinMessage(rerr)) 116 } 117 }() 118 119 v, err := valueF(i.Get(key)) 120 if err != nil { 121 return errs.Wrap(err, "valueF failed") 122 } 123 124 if v == CancelSet { 125 logging.Debug("setWithCallback cancelled") 126 return nil 127 } 128 129 q, err := i.db.Prepare(`INSERT OR REPLACE INTO config(key, value) VALUES(?,?)`) 130 if err != nil { 131 return errs.Wrap(err, "Could not modify settings") 132 } 133 defer q.Close() 134 135 valueMarshaled, err := yaml.Marshal(v) 136 if err != nil { 137 return errs.Wrap(err, "Could not marshal config value: %v", v) 138 } 139 140 _, err = q.Exec(key, valueMarshaled) 141 if err != nil { 142 return errs.Wrap(err, "Could not store setting") 143 } 144 145 return nil 146 } 147 148 // Set sets a value at the given key. 149 func (i *Instance) Set(key string, value interface{}) error { 150 return i.GetThenSet(key, func(_ interface{}) (interface{}, error) { 151 return value, nil 152 }) 153 } 154 155 func (i *Instance) IsSet(key string) bool { 156 return i.rawGet(key) != nil 157 } 158 159 func (i *Instance) rawGet(key string) interface{} { 160 row := i.db.QueryRow(`SELECT value FROM config WHERE key=?`, key) 161 if row.Err() != nil { 162 multilog.Error("config:get query failed: %s", errs.JoinMessage(row.Err())) 163 return nil 164 } 165 166 var value string 167 if err := row.Scan(&value); err != nil { 168 return nil // No results 169 } 170 171 var result interface{} 172 if err := yaml.Unmarshal([]byte(value), &result); err != nil { 173 if err2 := json.Unmarshal([]byte(value), &result); err2 != nil { 174 multilog.Error("config:get unmarshal failed: %s (json err: %s)", errs.JoinMessage(err), errs.JoinMessage(err2)) 175 return nil 176 } 177 } 178 179 return result 180 } 181 182 func (i *Instance) Get(key string) interface{} { 183 result := i.rawGet(key) 184 if result != nil { 185 return result 186 } 187 if opt := mediator.GetOption(key); mediator.KnownOption(opt) { 188 return opt.Default 189 } 190 return nil 191 } 192 193 // GetString retrieves a string for a given key 194 func (i *Instance) GetString(key string) string { 195 return cast.ToString(i.Get(key)) 196 } 197 198 // GetInt retrieves an int for a given key 199 func (i *Instance) GetInt(key string) int { 200 return cast.ToInt(i.Get(key)) 201 } 202 203 // AllKeys returns all of the curent config keys 204 func (i *Instance) AllKeys() []string { 205 rows, err := i.db.Query(`SELECT key FROM config`) 206 if err != nil { 207 multilog.Error("config:AllKeys query failed: %s", errs.JoinMessage(err)) 208 return nil 209 } 210 var keys []string 211 defer rows.Close() 212 for rows.Next() { 213 var key string 214 if err = rows.Scan(&key); err != nil { 215 multilog.Error("config:AllKeys scan failed: %s", errs.JoinMessage(err)) 216 return nil 217 } 218 keys = append(keys, key) 219 } 220 return keys 221 } 222 223 // GetStringMapStringSlice retrieves a map of string slices for a given key 224 func (i *Instance) GetStringMapStringSlice(key string) map[string][]string { 225 return cast.ToStringMapStringSlice(i.Get(key)) 226 } 227 228 // GetBool retrieves a boolean value for a given key 229 func (i *Instance) GetBool(key string) bool { 230 return cast.ToBool(i.Get(key)) 231 } 232 233 // GetStringSlice retrieves a slice of strings for a given key 234 func (i *Instance) GetStringSlice(key string) []string { 235 return cast.ToStringSlice(i.Get(key)) 236 } 237 238 // GetTime retrieves a time instance for a given key 239 func (i *Instance) GetTime(key string) time.Time { 240 return cast.ToTime(i.Get(key)) 241 } 242 243 // GetStringMap retrieves a map of strings to values for a given key 244 func (i *Instance) GetStringMap(key string) map[string]interface{} { 245 return cast.ToStringMap(i.Get(key)) 246 } 247 248 // ConfigPath returns the path at which our configuration is stored 249 func (i *Instance) ConfigPath() string { 250 return i.appDataDir 251 }