github.com/jfrog/jfrog-cli-core/v2@v2.52.0/utils/lock/lock.go (about) 1 package lock 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/jfrog/jfrog-cli-core/v2/utils/osutils" 14 "github.com/jfrog/jfrog-client-go/utils/errorutils" 15 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 16 "github.com/jfrog/jfrog-client-go/utils/log" 17 ) 18 19 type Lock struct { 20 // The current time when the lock was created 21 currentTime int64 22 // The full path to the lock file. 23 fileName string 24 pid int 25 } 26 27 type Locks []Lock 28 29 func (locks Locks) Len() int { 30 return len(locks) 31 } 32 33 func (locks Locks) Swap(i, j int) { 34 locks[i], locks[j] = locks[j], locks[i] 35 } 36 37 func (locks Locks) Less(i, j int) bool { 38 return locks[i].currentTime < locks[j].currentTime 39 } 40 41 // Creating a new lock object. 42 func (lock *Lock) createNewLockFile(lockDirPath string) error { 43 lock.currentTime = time.Now().UnixNano() 44 err := fileutils.CreateDirIfNotExist(lockDirPath) 45 if err != nil { 46 return err 47 } 48 lock.pid = os.Getpid() 49 return lock.createFile(lockDirPath) 50 } 51 52 func (lock *Lock) getLockFilename(folderName string) string { 53 return filepath.Join(folderName, "jfrog-cli.conf.lck."+strconv.Itoa(lock.pid)+"."+strconv.FormatInt(lock.currentTime, 10)) 54 } 55 56 func (lock *Lock) createFile(folderName string) error { 57 // We are creating an empty file with the pid and current time part of the name 58 lock.fileName = lock.getLockFilename(folderName) 59 file, err := os.OpenFile(lock.fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 60 if err != nil { 61 return errorutils.CheckError(err) 62 } 63 if err = file.Close(); err != nil { 64 return errorutils.CheckError(err) 65 } 66 return nil 67 } 68 69 // Try to acquire a lock 70 func (lock *Lock) lock() error { 71 filesList, err := lock.getListOfFiles() 72 if err != nil { 73 return err 74 } 75 i := 0 76 // Trying 1200 times to acquire a lock 77 for i <= 1200 { 78 // If only one file, means that the process that is running is the one that created the file. 79 // We can continue 80 if len(filesList) == 1 { 81 return nil 82 } 83 84 locks, err := getLocks(filesList) 85 if err != nil { 86 return err 87 } 88 // If the first timestamp in the sorted locks slice is equal to this timestamp 89 // means that the lock can be acquired 90 if locks[0].currentTime == lock.currentTime { 91 // Edge case, if at the same time (by the nanoseconds) two different process created two files. 92 // We are checking the PID to know which process can run. 93 if locks[0].pid != lock.pid { 94 err := lock.removeOtherLockOrWait(locks[0], &filesList) 95 if err != nil { 96 return err 97 } 98 } else { 99 log.Debug("Lock has been acquired for", lock.fileName) 100 return nil 101 } 102 } else { 103 err := lock.removeOtherLockOrWait(locks[0], &filesList) 104 if err != nil { 105 return err 106 } 107 } 108 i++ 109 } 110 return errors.New("lock hasn't been acquired") 111 } 112 113 // Checks if other lock file still exists. 114 // Or the process that created the lock still running. 115 func (lock *Lock) removeOtherLockOrWait(otherLock Lock, filesList *[]string) error { 116 // Check if file exists. 117 exists, err := fileutils.IsFileExists(otherLock.fileName, false) 118 if err != nil { 119 return err 120 } 121 122 if !exists { 123 // Process already finished. Update the list. 124 *filesList, err = lock.getListOfFiles() 125 if err != nil { 126 return err 127 } 128 return nil 129 } 130 log.Debug("Lock hasn't been acquired.") 131 132 // Check if the process is running. 133 // There are two implementation of the 'IsProcessRunning'. 134 // One for Windows and one for Unix based systems. 135 running, err := osutils.IsProcessRunning(otherLock.pid) 136 if err != nil { 137 return err 138 } 139 140 if !running { 141 log.Debug(fmt.Sprintf("Removing lock file %s since the creating process is no longer running", otherLock.fileName)) 142 err := otherLock.Unlock() 143 if err != nil { 144 return err 145 } 146 // Update list of files 147 *filesList, err = lock.getListOfFiles() 148 return err 149 } 150 // Other process is running. Wait. 151 time.Sleep(100 * time.Millisecond) 152 return nil 153 } 154 155 func (lock *Lock) getListOfFiles() ([]string, error) { 156 // Listing all the files in the lock directory 157 filesList, err := fileutils.ListFiles(filepath.Dir(lock.fileName), false) 158 if err != nil { 159 return nil, err 160 } 161 return filesList, nil 162 } 163 164 // Returns a list of all available locks. 165 func getLocks(filesList []string) (Locks, error) { 166 // Slice of all the timestamps that currently the lock directory has 167 var files Locks 168 for _, path := range filesList { 169 fileName := filepath.Base(path) 170 splitted := strings.Split(fileName, ".") 171 172 if len(splitted) != 5 { 173 return nil, errorutils.CheckErrorf("Failed while parsing the file name: %s located at: %s. Expecting a different format.", fileName, path) 174 } 175 // Last element is the timestamp. 176 time, err := strconv.ParseInt(splitted[4], 10, 64) 177 if err != nil { 178 return nil, errorutils.CheckError(err) 179 } 180 pid, err := strconv.Atoi(splitted[3]) 181 if err != nil { 182 return nil, errorutils.CheckError(err) 183 } 184 file := Lock{ 185 currentTime: time, 186 pid: pid, 187 fileName: path, 188 } 189 files = append(files, file) 190 } 191 sort.Sort(files) 192 return files, nil 193 } 194 195 // Removes the lock file so other process can continue. 196 func (lock *Lock) Unlock() error { 197 log.Debug("Releasing lock:", lock.fileName) 198 exists, err := fileutils.IsFileExists(lock.fileName, false) 199 if err != nil { 200 return err 201 } 202 203 if exists { 204 err = os.Remove(lock.fileName) 205 if err != nil { 206 return errorutils.CheckError(err) 207 } 208 } 209 return nil 210 } 211 212 func CreateLock(lockDirPath string) (unlock func() error, err error) { 213 log.Debug("Creating lock in:", lockDirPath) 214 lockFile := new(Lock) 215 unlock = func() error { return lockFile.Unlock() } 216 err = lockFile.createNewLockFile(lockDirPath) 217 if err != nil { 218 return 219 } 220 221 // Trying to acquire a lock for the running process. 222 err = lockFile.lock() 223 if err != nil { 224 err = errorutils.CheckError(err) 225 } 226 return 227 } 228 229 func GetLastLockTimestamp(lockDirPath string) (int64, error) { 230 filesList, err := fileutils.ListFiles(lockDirPath, false) 231 if err != nil { 232 return 0, err 233 } 234 if len(filesList) == 0 { 235 return 0, nil 236 } 237 locks, err := getLocks(filesList) 238 if err != nil || len(locks) == 0 { 239 return 0, err 240 } 241 242 lastLock := locks[len(locks)-1] 243 244 // If the lock isn't acquired by a running process, an unexpected error was occurred. 245 running, err := osutils.IsProcessRunning(lastLock.pid) 246 if err != nil { 247 return 0, err 248 } 249 if !running { 250 return 0, nil 251 } 252 253 return lastLock.currentTime, nil 254 }