kcl-lang.io/kpm@v0.8.7-0.20240520061008-9fc4c5efc8c7/pkg/settings/settings.go (about)

     1  package settings
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/gofrs/flock"
    14  	"kcl-lang.io/kpm/pkg/env"
    15  	"kcl-lang.io/kpm/pkg/errors"
    16  	"kcl-lang.io/kpm/pkg/reporter"
    17  	"kcl-lang.io/kpm/pkg/utils"
    18  )
    19  
    20  // The config.json used to persist user information
    21  const CONFIG_JSON_PATH = ".kpm/config/config.json"
    22  
    23  // The kpm.json used to describe the default configuration of kpm.
    24  const KPM_JSON_PATH = ".kpm/config/kpm.json"
    25  
    26  // The package-cache path, kpm will try lock 'package-cache' before downloading a package.
    27  const PACKAGE_CACHE_PATH = ".kpm/config/package-cache"
    28  
    29  // The kpm configuration
    30  type KpmConf struct {
    31  	DefaultOciRegistry  string
    32  	DefaultOciRepo      string
    33  	DefaultOciPlainHttp bool
    34  }
    35  
    36  const ON = "on"
    37  const OFF = "off"
    38  const DEFAULT_REGISTRY = "ghcr.io"
    39  const DEFAULT_REPO = "kcl-lang"
    40  const DEFAULT_OCI_PLAIN_HTTP = OFF
    41  const DEFAULT_REGISTRY_ENV = "KPM_REG"
    42  const DEFAULT_REPO_ENV = "KPM_REPO"
    43  const DEFAULT_OCI_PLAIN_HTTP_ENV = "OCI_REG_PLAIN_HTTP"
    44  
    45  // This is a singleton that loads kpm settings from 'kpm.json'
    46  // and is only initialized on the first call by 'Init()' or 'GetSettings()'
    47  var kpm_settings *Settings
    48  var once sync.Once
    49  
    50  // DefaultKpmConf create a default configuration for kpm.
    51  func DefaultKpmConf() KpmConf {
    52  	return KpmConf{
    53  		DefaultOciRegistry:  DEFAULT_REGISTRY,
    54  		DefaultOciRepo:      DEFAULT_REPO,
    55  		DefaultOciPlainHttp: DEFAULT_OCI_PLAIN_HTTP == ON,
    56  	}
    57  }
    58  
    59  type Settings struct {
    60  	CredentialsFile string
    61  	KpmConfFile     string
    62  	// the default configuration for kpm.
    63  	Conf KpmConf
    64  
    65  	// the flock used to lock the 'package-cache' file.
    66  	PackageCacheLock *flock.Flock
    67  
    68  	// the error catch from the closure in once.Do()
    69  	ErrorEvent *reporter.KpmEvent
    70  }
    71  
    72  // AcquirePackageCacheLock will try to lock the 'package-cache' file.
    73  func (settings *Settings) AcquirePackageCacheLock(logWriter io.Writer) error {
    74  	// if the 'package-cache' file is not initialized, this is an internal bug.
    75  	if settings.PackageCacheLock == nil {
    76  		return errors.InternalBug
    77  	}
    78  
    79  	// try to lock the 'package-cache' file
    80  	locked, err := settings.PackageCacheLock.TryLock()
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	// if failed to lock the 'package-cache' file, wait until it is unlocked.
    86  	if !locked {
    87  		reporter.ReportEventTo(reporter.NewEvent(reporter.WaitingLock, "waiting for package-cache lock..."), logWriter)
    88  		for {
    89  			// try to lock the 'package-cache' file
    90  			locked, err = settings.PackageCacheLock.TryLock()
    91  			if err != nil {
    92  				return err
    93  			}
    94  			// if locked, break the loop.
    95  			if locked {
    96  				break
    97  			}
    98  			// when waiting for a file lock, the program will continuously attempt to acquire the lock.
    99  			// without adding a sleep, the program will rapidly try to acquire the lock, consuming a large amount of CPU resources.
   100  			// by adding a sleep, the program can pause for a period of time between each attempt to acquire the lock,
   101  			// reducing the consumption of CPU resources.
   102  			time.Sleep(2 * time.Millisecond)
   103  		}
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // ReleasePackageCacheLock will try to unlock the 'package-cache' file.
   110  func (settings *Settings) ReleasePackageCacheLock() error {
   111  	// if the 'package-cache' file is not initialized, this is an internal bug.
   112  	if settings.PackageCacheLock == nil {
   113  		return errors.InternalBug
   114  	}
   115  
   116  	// try to unlock the 'package-cache' file.
   117  	err := settings.PackageCacheLock.Unlock()
   118  	if err != nil {
   119  		return err
   120  	}
   121  	return nil
   122  }
   123  
   124  // DefaultOciRepo return the default OCI registry 'ghcr.io'.
   125  func (settings *Settings) DefaultOciRegistry() string {
   126  	return settings.Conf.DefaultOciRegistry
   127  }
   128  
   129  // DefaultOciRepo return the default OCI repo 'kcl-lang'.
   130  func (settings *Settings) DefaultOciRepo() string {
   131  	return settings.Conf.DefaultOciRepo
   132  }
   133  
   134  // DefaultOciPlainHttp return the default OCI plain http 'false'.
   135  func (settings *Settings) DefaultOciPlainHttp() bool {
   136  	return settings.Conf.DefaultOciPlainHttp
   137  }
   138  
   139  // DefaultOciRef return the default OCI ref 'ghcr.io/kcl-lang'.
   140  func (settings *Settings) DefaultOciRef() string {
   141  	return utils.JoinPath(settings.Conf.DefaultOciRegistry, settings.Conf.DefaultOciRepo)
   142  }
   143  
   144  // LoadSettingsFromEnv will load the kpm settings from environment variables.
   145  func (settings *Settings) LoadSettingsFromEnv() (*Settings, *reporter.KpmEvent) {
   146  	// Load the env KPM_REG
   147  	reg := os.Getenv(DEFAULT_REGISTRY_ENV)
   148  	if len(reg) > 0 {
   149  		settings.Conf.DefaultOciRegistry = reg
   150  	}
   151  	// Load the env KPM_REPO
   152  	repo := os.Getenv(DEFAULT_REPO_ENV)
   153  	if len(repo) > 0 {
   154  		settings.Conf.DefaultOciRepo = repo
   155  	}
   156  
   157  	// Load the env OCI_REG_PLAIN_HTTP
   158  	plainHttp := os.Getenv(DEFAULT_OCI_PLAIN_HTTP_ENV)
   159  	var err error
   160  	if len(plainHttp) > 0 {
   161  		settings.Conf.DefaultOciPlainHttp, err = isOn(plainHttp)
   162  		if err != nil {
   163  			return settings, reporter.NewErrorEvent(
   164  				reporter.UnknownEnv,
   165  				err,
   166  				fmt.Sprintf("unknown environment variable '%s=%s'", DEFAULT_OCI_PLAIN_HTTP_ENV, plainHttp),
   167  			)
   168  		}
   169  	}
   170  	return settings, nil
   171  }
   172  
   173  func isOn(input string) (bool, error) {
   174  	if strings.ToLower(input) == ON {
   175  		return true, nil
   176  	} else if strings.ToLower(input) == OFF {
   177  		return false, nil
   178  	} else {
   179  		return false, errors.UnknownEnv
   180  	}
   181  }
   182  
   183  // GetFullPath returns the full path file path under '$HOME/.kpm/config/'
   184  func GetFullPath(jsonFileName string) (string, error) {
   185  	home, err := env.GetAbsPkgPath()
   186  	if err != nil {
   187  		return "", reporter.NewErrorEvent(reporter.Bug, err, "internal bugs, failed to load working directory")
   188  	}
   189  
   190  	return filepath.Join(home, jsonFileName), nil
   191  }
   192  
   193  // GetSettings will return the kpm setting singleton.
   194  func GetSettings() *Settings {
   195  	once.Do(func() {
   196  		kpm_settings = &Settings{}
   197  		credentialsFile, err := GetFullPath(CONFIG_JSON_PATH)
   198  		if err != nil {
   199  			kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   200  				reporter.FailedLoadSettings,
   201  				err,
   202  				fmt.Sprintf("failed to load config file '%s' for kpm.", credentialsFile),
   203  			)
   204  			return
   205  		}
   206  		kpm_settings.CredentialsFile = credentialsFile
   207  		kpm_settings.KpmConfFile, err = GetFullPath(KPM_JSON_PATH)
   208  		if err != nil {
   209  			kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   210  				reporter.FailedLoadSettings,
   211  				err,
   212  				fmt.Sprintf("failed to load config file '%s' for kpm.", kpm_settings.KpmConfFile),
   213  			)
   214  			return
   215  		}
   216  
   217  		conf, err := loadOrCreateDefaultKpmJson()
   218  		if err != nil {
   219  			kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   220  				reporter.FailedLoadSettings,
   221  				err,
   222  				fmt.Sprintf("failed to load config file '%s' for kpm.", kpm_settings.KpmConfFile),
   223  			)
   224  			return
   225  		}
   226  
   227  		lockPath, err := GetFullPath(PACKAGE_CACHE_PATH)
   228  		if err != nil {
   229  			kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   230  				reporter.FailedLoadSettings,
   231  				err,
   232  				fmt.Sprintf("failed to load config file '%s' for kpm.", lockPath),
   233  			)
   234  			return
   235  		}
   236  
   237  		// If the 'lockPath' file exists, do nothing.
   238  		// If the 'lockPath' file does not exist, recursively create the 'lockPath' path.
   239  		// If the 'lockPath' path cannot be created, return an error.
   240  		// 'lockPath' is a file path not a directory path.
   241  		if !utils.DirExists(lockPath) {
   242  			// recursively create the 'lockPath' path.
   243  			err = os.MkdirAll(filepath.Dir(lockPath), 0755)
   244  			if err != nil {
   245  				kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   246  					reporter.FailedLoadSettings,
   247  					err,
   248  					fmt.Sprintf("failed to create lock file '%s' for kpm.", lockPath),
   249  				)
   250  				return
   251  			}
   252  			// create a empty file named 'package-cache'.
   253  			_, err = os.Create(lockPath)
   254  			if err != nil {
   255  				kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   256  					reporter.FailedLoadSettings,
   257  					err,
   258  					fmt.Sprintf("failed to create lock file '%s' for kpm.", lockPath),
   259  				)
   260  				return
   261  			}
   262  		}
   263  
   264  		kpm_settings.Conf = *conf
   265  		kpm_settings.PackageCacheLock = flock.New(lockPath)
   266  	})
   267  
   268  	kpm_settings, err := kpm_settings.LoadSettingsFromEnv()
   269  	if err != (*reporter.KpmEvent)(nil) {
   270  		if kpm_settings.ErrorEvent != (*reporter.KpmEvent)(nil) {
   271  			kpm_settings.ErrorEvent = reporter.NewErrorEvent(
   272  				reporter.UnknownEnv,
   273  				err,
   274  			)
   275  		} else {
   276  			kpm_settings.ErrorEvent = err
   277  		}
   278  	}
   279  
   280  	return kpm_settings
   281  }
   282  
   283  // loadOrCreateDefaultKpmJson will load the 'kpm.json' file from '$KCL_PKG_PATH/.kpm/config',
   284  // and create a default 'kpm.json' file if the file does not exist.
   285  func loadOrCreateDefaultKpmJson() (*KpmConf, error) {
   286  	kpmConfpath, err := GetFullPath(KPM_JSON_PATH)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	defaultKpmConf := DefaultKpmConf()
   292  
   293  	b, err := os.ReadFile(kpmConfpath)
   294  	// if the file '$KCL_PKG_PATH/.kpm/config/kpm.json' does not exist
   295  	if os.IsNotExist(err) {
   296  		// create the default kpm.json.
   297  		err = os.MkdirAll(filepath.Dir(kpmConfpath), 0755)
   298  		if err != nil {
   299  			return nil, err
   300  		}
   301  
   302  		b, err := json.Marshal(defaultKpmConf)
   303  		if err != nil {
   304  			return nil, err
   305  		}
   306  		err = os.WriteFile(kpmConfpath, b, 0644)
   307  		if err != nil {
   308  			return nil, err
   309  		}
   310  		return &defaultKpmConf, nil
   311  	} else {
   312  		err = json.Unmarshal(b, &defaultKpmConf)
   313  		if err != nil {
   314  			return nil, err
   315  		}
   316  		return &defaultKpmConf, nil
   317  	}
   318  }