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 }