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  }