-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixing config file lock mechanism #352
Changes from all commits
cbc3a4e
9ce9fc6
e586722
fb3ec08
adeda57
f46d94b
d9957d2
1188b66
0bbf0ed
f3beb70
cd08d02
4f7319a
ff730fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,26 +2,34 @@ package commands | |
|
||
import ( | ||
"fmt" | ||
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" | ||
"github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" | ||
"github.com/jfrog/jfrog-cli-core/v2/utils/lock" | ||
"io/ioutil" | ||
"reflect" | ||
"strconv" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" | ||
"github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" | ||
|
||
"github.com/jfrog/jfrog-client-go/auth" | ||
|
||
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" | ||
"github.com/jfrog/jfrog-cli-core/v2/utils/config" | ||
"github.com/jfrog/jfrog-cli-core/v2/utils/lock" | ||
clientutils "github.com/jfrog/jfrog-client-go/utils" | ||
"github.com/jfrog/jfrog-client-go/utils/errorutils" | ||
"github.com/jfrog/jfrog-client-go/utils/io/fileutils" | ||
"github.com/jfrog/jfrog-client-go/utils/log" | ||
) | ||
|
||
type ConfigAction string | ||
|
||
const ( | ||
AddOrEdit ConfigAction = "AddOrEdit" | ||
Delete ConfigAction = "Delete" | ||
Use ConfigAction = "Use" | ||
Clear ConfigAction = "Clear" | ||
) | ||
|
||
// Internal golang locking for the same process. | ||
var mutex sync.Mutex | ||
|
||
|
@@ -34,10 +42,11 @@ type ConfigCommand struct { | |
serverId string | ||
// For unit tests | ||
disablePromptUrls bool | ||
cmdType ConfigAction | ||
} | ||
|
||
func NewConfigCommand() *ConfigCommand { | ||
return &ConfigCommand{} | ||
func NewConfigCommand(cmdType ConfigAction, serverId string) *ConfigCommand { | ||
return &ConfigCommand{cmdType: cmdType, serverId: serverId} | ||
} | ||
|
||
func (cc *ConfigCommand) SetServerId(serverId string) *ConfigCommand { | ||
|
@@ -71,7 +80,34 @@ func (cc *ConfigCommand) SetDetails(details *config.ServerDetails) *ConfigComman | |
} | ||
|
||
func (cc *ConfigCommand) Run() error { | ||
return cc.Config() | ||
mutex.Lock() | ||
defer mutex.Unlock() | ||
lockDirPath, err := coreutils.GetJfrogConfigLockDir() | ||
if err != nil { | ||
return err | ||
} | ||
lockFile, err := lock.CreateLock(lockDirPath) | ||
defer func() { | ||
e := lockFile.Unlock() | ||
if err == nil { | ||
err = e | ||
} | ||
}() | ||
if err != nil { | ||
return err | ||
} | ||
switch cc.cmdType { | ||
case AddOrEdit: | ||
return cc.config() | ||
case Delete: | ||
return cc.delete() | ||
case Use: | ||
return cc.use() | ||
case Clear: | ||
return cc.clear() | ||
default: | ||
return fmt.Errorf("Not supported config command type: " + string(cc.cmdType)) | ||
} | ||
} | ||
|
||
func (cc *ConfigCommand) ServerDetails() (*config.ServerDetails, error) { | ||
|
@@ -90,20 +126,7 @@ func (cc *ConfigCommand) CommandName() string { | |
return "config" | ||
} | ||
|
||
func (cc *ConfigCommand) Config() error { | ||
mutex.Lock() | ||
defer mutex.Unlock() | ||
lockDirPath, err := coreutils.GetJfrogConfigLockDir() | ||
if err != nil { | ||
return err | ||
} | ||
lockFile, err := lock.CreateLock(lockDirPath) | ||
defer lockFile.Unlock() | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
func (cc *ConfigCommand) config() error { | ||
configurations, err := cc.prepareConfigurationData() | ||
if err != nil { | ||
return err | ||
|
@@ -134,7 +157,7 @@ func (cc *ConfigCommand) Config() error { | |
|
||
// Artifactory expects the username to be lower-cased. In case it is not, | ||
// Artifactory will silently save it lower-cased, but the token creation | ||
// REST API will fail with a non lower-cased username. | ||
// REST API will fail with a non-lower-cased username. | ||
cc.details.User = strings.ToLower(cc.details.User) | ||
|
||
if len(configurations) == 1 { | ||
|
@@ -211,7 +234,7 @@ func (cc *ConfigCommand) prepareConfigurationData() ([]*config.ServerDetails, er | |
return configurations, err | ||
} | ||
|
||
/// Returning the first non empty value: | ||
/// Returning the first non-empty value: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /// --> // |
||
// 1. The serverId argument sent. | ||
// 2. details.ServerId | ||
// 3. defaultDetails.ServerId | ||
|
@@ -388,7 +411,7 @@ func Import(serverToken string) error { | |
details: serverDetails, | ||
serverId: serverDetails.ServerId, | ||
} | ||
return configCommand.Config() | ||
return configCommand.config() | ||
} | ||
|
||
func Export(serverName string) error { | ||
|
@@ -438,14 +461,14 @@ func logIfNotEmpty(value, prefix string, mask bool) { | |
} | ||
} | ||
|
||
func DeleteConfig(serverName string) error { | ||
func (cc *ConfigCommand) delete() error { | ||
configurations, err := config.GetAllServersConfigs() | ||
if err != nil { | ||
return err | ||
} | ||
var isDefault, isFoundName bool | ||
for i, serverDetails := range configurations { | ||
if serverDetails.ServerId == serverName { | ||
if serverDetails.ServerId == cc.serverId { | ||
isDefault = serverDetails.IsDefault | ||
configurations = append(configurations[:i], configurations[i+1:]...) | ||
isFoundName = true | ||
|
@@ -459,20 +482,20 @@ func DeleteConfig(serverName string) error { | |
if isFoundName { | ||
return config.SaveServersConf(configurations) | ||
} | ||
log.Info("\"" + serverName + "\" configuration could not be found.\n") | ||
log.Info("\"" + cc.serverId + "\" configuration could not be found.\n") | ||
return nil | ||
} | ||
|
||
// Set the default configuration | ||
func Use(serverId string) error { | ||
func (cc *ConfigCommand) use() error { | ||
configurations, err := config.GetAllServersConfigs() | ||
if err != nil { | ||
return err | ||
} | ||
var serverFound *config.ServerDetails | ||
newDefaultServer := true | ||
for _, serverDetails := range configurations { | ||
if serverDetails.ServerId == serverId { | ||
if serverDetails.ServerId == cc.serverId { | ||
serverFound = serverDetails | ||
if serverDetails.IsDefault { | ||
newDefaultServer = false | ||
|
@@ -494,11 +517,11 @@ func Use(serverId string) error { | |
log.Info(fmt.Sprintf("Using server ID '%s' (%s).", serverFound.ServerId, serverFound.Url)) | ||
return nil | ||
} | ||
return errorutils.CheckErrorf("Could not find a server with ID '%s'.", serverId) | ||
return errorutils.CheckErrorf("Could not find a server with ID '%s'.", cc.serverId) | ||
} | ||
|
||
func ClearConfig(interactive bool) error { | ||
if interactive { | ||
func (cc *ConfigCommand) clear() error { | ||
if cc.interactive { | ||
confirmed := coreutils.AskYesNo("Are you sure you want to delete all the configurations?", false) | ||
if !confirmed { | ||
return nil | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -38,7 +38,6 @@ func (locks Locks) Less(i, j int) bool { | |||||
|
||||||
// Creating a new lock object. | ||||||
func (lock *Lock) createNewLockFile(lockDirPath string) error { | ||||||
lock.currentTime = time.Now().UnixNano() | ||||||
err := fileutils.CreateDirIfNotExist(lockDirPath) | ||||||
if err != nil { | ||||||
return err | ||||||
|
@@ -50,13 +49,17 @@ func (lock *Lock) createNewLockFile(lockDirPath string) error { | |||||
|
||||||
func (lock *Lock) createFile(folderName string, pid int) error { | ||||||
// We are creating an empty file with the pid and current time part of the name | ||||||
lock.fileName = filepath.Join(folderName, "jfrog-cli.conf.lck."+strconv.Itoa(pid)+"."+strconv.FormatInt(lock.currentTime, 10)) | ||||||
lock.fileName = filepath.Join(folderName, "jfrog-cli.conf.lck."+strconv.Itoa(pid)+"."+strconv.FormatInt(time.Now().UnixNano(), 10)) | ||||||
log.Debug("Creating lock file: ", lock.fileName) | ||||||
file, err := os.OpenFile(lock.fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) | ||||||
if err != nil { | ||||||
return errorutils.CheckError(err) | ||||||
} | ||||||
|
||||||
fileInfo, err := file.Stat() | ||||||
if err != nil { | ||||||
return errorutils.CheckError(err) | ||||||
} | ||||||
lock.currentTime = fileInfo.ModTime().UnixNano() | ||||||
if err = file.Close(); err != nil { | ||||||
return errorutils.CheckError(err) | ||||||
} | ||||||
|
@@ -85,7 +88,7 @@ func (lock *Lock) lock() error { | |||||
// If the first timestamp in the sorted locks slice is equal to this timestamp | ||||||
// means that the lock can be acquired | ||||||
if locks[0].currentTime == lock.currentTime { | ||||||
// Edge case, if at the same time (by the nano seconds) two different process created two files. | ||||||
// Edge case, if at the same time (by the nanoseconds) two different process created two files. | ||||||
// We are checking the PID to know which process can run. | ||||||
if locks[0].pid != lock.pid { | ||||||
err := lock.removeOtherLockOrWait(locks[0], &filesList) | ||||||
|
@@ -163,23 +166,24 @@ func (lock *Lock) getLocks(filesList []string) (Locks, error) { | |||||
// Slice of all the timestamps that currently the lock directory has | ||||||
var files Locks | ||||||
for _, path := range filesList { | ||||||
fileName := filepath.Base(path) | ||||||
splitted := strings.Split(fileName, ".") | ||||||
|
||||||
if len(splitted) != 5 { | ||||||
return nil, errorutils.CheckErrorf("Failed while parsing the file name: %s located at: %s. Expecting a different format.", fileName, path) | ||||||
} | ||||||
// Last element is the timestamp. | ||||||
time, err := strconv.ParseInt(splitted[4], 10, 64) | ||||||
fileInfo, err := os.Stat(path) | ||||||
if err != nil { | ||||||
if os.IsNotExist(err) { | ||||||
// If file doesn't exist, then lock already deleted | ||||||
continue | ||||||
} | ||||||
return nil, errorutils.CheckError(err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Between getting the filesList and calling stat on the file, the other process may well be already done. In that case the file does not exist anymore which is normal and no cause for concern. As such, this error should be ignored here:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @HWoidt, I improved the Stat section and I will try to look again at the timing issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
} | ||||||
splitted := strings.Split(fileInfo.Name(), ".") | ||||||
if len(splitted) != 5 { | ||||||
return nil, errorutils.CheckErrorf("Failed while parsing the file name: %s located at: %s. Expecting a different format.", fileInfo.Name(), path) | ||||||
} | ||||||
pid, err := strconv.Atoi(splitted[3]) | ||||||
if err != nil { | ||||||
return nil, errorutils.CheckError(err) | ||||||
} | ||||||
file := Lock{ | ||||||
currentTime: time, | ||||||
currentTime: fileInfo.ModTime().UnixNano(), | ||||||
pid: pid, | ||||||
fileName: path, | ||||||
} | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not supported... --> Unsupported...