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  }