github.com/sercand/please@v13.4.0+incompatible/src/core/lock.go (about)

     1  // Contains utility functions for managing an exclusive lock file.
     2  // Based on flock() underneath so
     3  
     4  package core
     5  
     6  import (
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path"
    11  	"strings"
    12  	"syscall"
    13  
    14  	"github.com/pkg/xattr"
    15  )
    16  
    17  const lockFilePath = "plz-out/.lock"
    18  
    19  var lockFile *os.File
    20  
    21  // AcquireRepoLock opens the lock file and acquires the lock.
    22  // Dies if the lock cannot be successfully acquired.
    23  func AcquireRepoLock() {
    24  	var err error
    25  	// There is of course technically a bit of a race condition between the file & flock operations here,
    26  	// but it shouldn't matter much since we're trying to mutually exclude plz processes started by the user
    27  	// which (one hopes) they wouldn't normally do simultaneously.
    28  	os.MkdirAll(path.Dir(lockFilePath), DirPermissions)
    29  	// TODO(pebers): This doesn't seem quite as intended, I think the file still gets truncated sometimes.
    30  	//               Not sure why since I'm not passing O_TRUNC...
    31  	if lockFile, err = os.OpenFile(lockFilePath, os.O_RDWR|os.O_CREATE, 0644); err != nil && !os.IsNotExist(err) {
    32  		log.Fatalf("Failed to acquire lock: %s", err)
    33  	} else if lockFile, err = os.Create(lockFilePath); err != nil {
    34  		log.Fatalf("Failed to create lock: %s", err)
    35  	}
    36  	// Try a non-blocking acquire first so we can warn the user if we're waiting.
    37  	log.Debug("Attempting to acquire lock %s...", lockFilePath)
    38  	if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == nil {
    39  		log.Debug("Acquired lock %s", lockFilePath)
    40  	} else {
    41  		log.Warning("Looks like another plz is already running in this repo. Waiting for it to finish...")
    42  		if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX); err != nil {
    43  			log.Fatalf("Failed to acquire lock: %s", err)
    44  		}
    45  	}
    46  
    47  	// Record the operation performed.
    48  	if _, err = lockFile.Seek(0, io.SeekStart); err == nil {
    49  		if n, err := lockFile.Write([]byte(strings.Join(os.Args[1:], " ") + "\n")); err == nil {
    50  			lockFile.Truncate(int64(n))
    51  		}
    52  	}
    53  
    54  	// Quick test of xattrs; everything will fail later if we can't write them, so make
    55  	// sure plz-out supports them now and give a clear error if not.
    56  	if err := xattr.Set(lockFilePath, "user.plz_build", []byte("lock")); err != nil {
    57  		log.Fatalf("Failed to set xattrs:%s\nMaybe plz-out is on a filesystem that doesn't support them?", err)
    58  	}
    59  }
    60  
    61  // ReleaseRepoLock releases the lock and closes the file handle.
    62  // Does not die on errors, at this point it wouldn't really do any good.
    63  func ReleaseRepoLock() {
    64  	if lockFile == nil {
    65  		log.Errorf("Lock file not acquired!")
    66  		return
    67  	}
    68  	if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN); err != nil {
    69  		log.Errorf("Failed to release lock: %s", err) // No point making this fatal really
    70  	}
    71  	if err := lockFile.Close(); err != nil {
    72  		log.Errorf("Failed to close lock file: %s", err)
    73  	}
    74  }
    75  
    76  // ReadLastOperationOrDie reads the last operation performed from the lock file. Dies if unsuccessful.
    77  func ReadLastOperationOrDie() []string {
    78  	contents, err := ioutil.ReadFile(lockFilePath)
    79  	if err != nil || len(contents) == 0 {
    80  		log.Fatalf("Sorry OP, can't read previous operation :(")
    81  	}
    82  	return strings.Split(strings.TrimSpace(string(contents)), " ")
    83  }