github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/osutils/lockfile/pidlock.go (about)

     1  package lockfile
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/ActiveState/cli/internal/errs"
    11  	"github.com/ActiveState/cli/internal/osutils/stacktrace"
    12  )
    13  
    14  // PidLock represents a lock file that can be used for exclusive access to
    15  // resources that should be accessed by only one process at a time.
    16  //
    17  // The characteristics of the lock are:
    18  // - Lockfiles are removed after use
    19  // - Even if the lockfiles are not removed (because a process has been terminated prematurely), it is unlocked
    20  // - On file-systems that support advisory locks via fcntl or LockFileEx, all file system operations are atomic
    21  //
    22  // Notes:
    23  // - The implementation currently does not support a blocking wait operation that returns once the lock can be acquired. If required, it can be extended this way.
    24  // - Storing the PID inside the lockfile was initially intended to be fall-back mechanism for file systems that do not support locking files.  This is probably unnecessary, but could be extended to communicate with the process currently holding the lock via its PID.
    25  type PidLock struct {
    26  	path   string
    27  	file   *os.File
    28  	locked bool
    29  }
    30  
    31  // NewPidLock creates a new PidLock that can be used to get exclusive access to resources between processes
    32  func NewPidLock(path string) (pl *PidLock, err error) {
    33  	f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0666)
    34  	if err != nil {
    35  		return nil, errs.Wrap(err, "failed to open lock file %s", path)
    36  	}
    37  	return &PidLock{
    38  		path: path,
    39  		file: f,
    40  	}, nil
    41  }
    42  
    43  // TryLock attempts to lock the created lock file.
    44  func (pl *PidLock) TryLock() (err error) {
    45  	err = LockFile(pl.file)
    46  	if err != nil {
    47  		// if lock cannot be acquired it means that another process is holding the lock
    48  		return NewAlreadyLockedError(err, pl.path, "cannot acquire exclusive lock")
    49  	}
    50  
    51  	// check if PID can be read and if so, if the process is running
    52  	b := make([]byte, 100)
    53  	n, err := pl.file.Read(b)
    54  	if err != nil && err != io.EOF {
    55  		_ = LockRelease(pl.file)
    56  		return errs.Wrap(err, "failed to read PID from lockfile %s", pl.path)
    57  	}
    58  	if n > 0 {
    59  		pid, err := strconv.ParseInt(string(b[:n]), 10, 64)
    60  		if err != nil {
    61  			_ = LockRelease(pl.file)
    62  			return errs.Wrap(err, "failed to parse PID from lockfile %s", pl.path)
    63  		}
    64  		if PidExists(int(pid)) {
    65  			_ = LockRelease(pl.file)
    66  			err := fmt.Errorf("pid %d exists", pid)
    67  			return NewAlreadyLockedError(err, pl.path, "pid parsed")
    68  		}
    69  	}
    70  
    71  	// write PID into lock file
    72  	_, err = pl.file.Write([]byte(fmt.Sprintf("%d", os.Getpid())))
    73  	if err != nil {
    74  		_ = LockRelease(pl.file)
    75  		return errs.Wrap(err, "failed to write pid to lockfile %s", pl.path)
    76  	}
    77  
    78  	pl.locked = true
    79  	return nil
    80  }
    81  
    82  // Close removes the lock file and releases the lock
    83  func (pl *PidLock) Close(keepFile ...bool) error {
    84  	keep := false
    85  	if len(keepFile) == 1 {
    86  		keep = keepFile[0]
    87  	}
    88  	if !pl.locked {
    89  		err := pl.file.Close()
    90  		if err != nil {
    91  			return errs.Wrap(err, "failed to close unlocked lock file %s", pl.path)
    92  		}
    93  		return nil
    94  	}
    95  	err := pl.cleanLockFile(keep)
    96  	if err != nil {
    97  		return errs.Wrap(err, "failed to remove lock file")
    98  	}
    99  	return nil
   100  }
   101  
   102  // WaitForLock will attempt to acquire the lock for the duration given
   103  func (pl *PidLock) WaitForLock(timeout time.Duration) error {
   104  	expiration := time.Now().Add(timeout)
   105  	for {
   106  		err := pl.TryLock()
   107  		if err != nil {
   108  			if !errs.Matches(err, &AlreadyLockedError{}) {
   109  				return errs.Wrap(err, "Could not acquire lock")
   110  			}
   111  
   112  			if time.Now().After(expiration) {
   113  				return errs.Wrap(err, "Timed out trying to acquire lock")
   114  			}
   115  			time.Sleep(100 * time.Millisecond)
   116  			continue
   117  		}
   118  		return nil
   119  	}
   120  }
   121  
   122  // AlreadyLockedError manages info that clarifies why a lock has failed, but
   123  // is still likely valid.
   124  type AlreadyLockedError struct {
   125  	err   error
   126  	file  string
   127  	msg   string
   128  	stack *stacktrace.Stacktrace
   129  }
   130  
   131  // NewAlreadyLockedError returns a new AlreadyLockedError.
   132  func NewAlreadyLockedError(err error, file, msg string) *AlreadyLockedError {
   133  	return &AlreadyLockedError{err, file, msg, stacktrace.Get()}
   134  }
   135  
   136  // Error implements the error interface.
   137  func (e *AlreadyLockedError) Error() string {
   138  	return fmt.Sprintf("file %q is already locked: %s", e.file, e.msg)
   139  }
   140  
   141  // Unwrap allows the unwrapping of a causing error.
   142  func (e *AlreadyLockedError) Unwrap() error {
   143  	return e.err
   144  }
   145  
   146  // Stack implements the errs.WrapperError interface.
   147  func (e *AlreadyLockedError) Stack() *stacktrace.Stacktrace {
   148  	return e.stack
   149  }