gitlab.com/apertussolutions/u-root@v7.0.0+incompatible/pkg/lockfile/lockfile.go (about)

     1  // Copyright 2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package lockfile coordinates process-based file locking.
     6  //
     7  // This package is designed to aid concurrency issues between different
     8  // processes.
     9  //
    10  // Sample usage:
    11  //
    12  //	lf := lockfile.New("/var/apt/apt.lock")
    13  //	// Blocks and waits if /var/apt/apt.lock already exists.
    14  //	if err := lf.Lock(); err != nil {
    15  //		log.Fatal(err)
    16  //	}
    17  //
    18  //	defer lf.MustUnlock()
    19  //
    20  //	// Do something in /var/apt/??
    21  //
    22  // Two concurrent invocations of this program will compete to create
    23  // /var/apt/apt.lock, and then make the other wait until /var/apt/apt.lock
    24  // disappears.
    25  //
    26  // If the lock holding process disappears without removing the lock, another
    27  // process using this library will detect that and remove the lock.
    28  //
    29  // If some other entity removes the lockfile erroneously, the lock holder's
    30  // call to Unlock() will return an ErrRogueDeletion.
    31  package lockfile
    32  
    33  import (
    34  	"errors"
    35  	"fmt"
    36  	"io"
    37  	"io/ioutil"
    38  	"log"
    39  	"os"
    40  	"path/filepath"
    41  	"strconv"
    42  	"syscall"
    43  )
    44  
    45  var (
    46  	// ErrRogueDeletion means the lock file was removed by someone
    47  	// other than the lock holder.
    48  	ErrRogueDeletion = errors.New("cannot unlock lockfile owned by another process")
    49  
    50  	// ErrBusy means the lock is being held by another living process.
    51  	ErrBusy = errors.New("file is locked by another process")
    52  
    53  	// ErrInvalidPID means the lock file contains an incompatible syntax.
    54  	ErrInvalidPID = errors.New("lockfile points to file with invalid content")
    55  
    56  	// ErrProcessDead means the lock is held by a process that does not exist.
    57  	ErrProcessDead = errors.New("lockfile points to invalid PID")
    58  )
    59  
    60  var (
    61  	errUnlocked = errors.New("file is unlocked")
    62  )
    63  
    64  // Lockfile is a process-based file lock.
    65  type Lockfile struct {
    66  	// path is the file whose existense is the lock.
    67  	path string
    68  
    69  	// pid is our PID.
    70  	//
    71  	// This mostly exists for testing.
    72  	pid int
    73  }
    74  
    75  // New returns a new lock file at the given path.
    76  func New(path string) *Lockfile {
    77  	return &Lockfile{
    78  		path: path,
    79  		pid:  os.Getpid(),
    80  	}
    81  }
    82  
    83  func (l *Lockfile) pidfile() (string, error) {
    84  	dir, base := filepath.Split(l.path)
    85  	pidfile, err := ioutil.TempFile(dir, fmt.Sprintf("%s-", base))
    86  	if err != nil {
    87  		return "", err
    88  	}
    89  	defer pidfile.Close()
    90  
    91  	if _, err := io.WriteString(pidfile, fmt.Sprintf("%d", l.pid)); err != nil {
    92  		if err := os.Remove(pidfile.Name()); err != nil {
    93  			log.Fatalf("Lockfile could not remove %q: %v", pidfile.Name(), err)
    94  		}
    95  		return "", err
    96  	}
    97  	return pidfile.Name(), nil
    98  }
    99  
   100  // TryLock attempts to create the lock file, or returns ErrBusy if a valid lock
   101  // file already exists.
   102  //
   103  // If the lock file is detected not to be valid, it is removed and replaced
   104  // with our lock file.
   105  func (l *Lockfile) TryLock() error {
   106  	pidpath, err := l.pidfile()
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	if err := l.lockWith(pidpath); err != nil {
   112  		if err := os.Remove(pidpath); err != nil {
   113  			log.Fatalf("Lockfile could not remove %q: %v", pidpath, err)
   114  		}
   115  		return err
   116  	}
   117  	return nil
   118  }
   119  
   120  // Lock blocks until it can create a valid lock file.
   121  //
   122  // If a valid lock file already exists, it waits for the file to be deleted or
   123  // until the associated process dies.
   124  //
   125  // If an invalid lock file exists, it will be deleted and we'll retry locking.
   126  func (l *Lockfile) Lock() error {
   127  	pidpath, err := l.pidfile()
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// Spin, oh, spin.
   133  	if err := l.lockWith(pidpath); err == ErrBusy {
   134  		return l.lockWith(pidpath)
   135  	} else if err != nil {
   136  		if err := os.Remove(pidpath); err != nil {
   137  			log.Fatalf("Lockfile could not remove %q: %v", pidpath, err)
   138  		}
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  func (l *Lockfile) checkLockfile() error {
   145  	owningPid, err := ioutil.ReadFile(l.path)
   146  	if os.IsNotExist(err) {
   147  		return errUnlocked
   148  	} else if err != nil {
   149  		return err
   150  	}
   151  
   152  	if len(owningPid) == 0 {
   153  		return ErrInvalidPID
   154  	}
   155  
   156  	pid, err := strconv.Atoi(string(owningPid))
   157  	if err != nil || pid <= 0 {
   158  		return ErrInvalidPID
   159  	}
   160  
   161  	p, err := os.FindProcess(pid)
   162  	if err != nil {
   163  		return ErrProcessDead
   164  	}
   165  
   166  	if err := p.Signal(syscall.Signal(0)); err != nil {
   167  		return ErrProcessDead
   168  	}
   169  
   170  	if pid == l.pid {
   171  		return nil
   172  	}
   173  	return ErrBusy
   174  }
   175  
   176  // Unlock attempts to delete the lock file.
   177  //
   178  // If we are not the lock holder, and the lock holder is an existing process,
   179  // ErrRogueDeletion will be returned.
   180  //
   181  // If we are not the lock holder, and there is no valid lock holder (process
   182  // died, invalid lock file syntax), ErrRogueDeletion will be returned.
   183  func (l *Lockfile) Unlock() error {
   184  	switch err := l.checkLockfile(); err {
   185  	case nil:
   186  		// Nuke the symlink and its target.
   187  		target, err := os.Readlink(l.path)
   188  		if os.IsNotExist(err) {
   189  			return ErrRogueDeletion
   190  		} else if err != nil {
   191  			// The symlink is somehow screwed up. Just nuke it.
   192  			if err := os.Remove(l.path); err != nil {
   193  				log.Fatalf("Lockfile could not remove %q: %v", l.path, err)
   194  			}
   195  			return err
   196  		}
   197  
   198  		absTarget := resolveSymlinkTarget(l.path, target)
   199  		if err := os.Remove(absTarget); os.IsNotExist(err) {
   200  			return ErrRogueDeletion
   201  		} else if err != nil {
   202  			return err
   203  		}
   204  
   205  		if err := os.Remove(l.path); os.IsNotExist(err) {
   206  			return ErrRogueDeletion
   207  		} else if err != nil {
   208  			return err
   209  		}
   210  		return nil
   211  
   212  	case ErrInvalidPID, ErrProcessDead, errUnlocked, ErrBusy:
   213  		return ErrRogueDeletion
   214  
   215  	default:
   216  		return err
   217  	}
   218  }
   219  
   220  // MustUnlock panics if the call to Unlock fails.
   221  func (l *Lockfile) MustUnlock() {
   222  	if err := l.Unlock(); err != nil {
   223  		log.Fatalf("could not unlock %q: %v", l.path, err)
   224  	}
   225  }
   226  
   227  // resolveSymlinkTarget returns an absolute path for a given symlink target.
   228  //
   229  // Symlinks targets' "working directory" is the symlink's parent.
   230  //
   231  // Said another way, a symlink is always resolved relative to the symlink's
   232  // parent.
   233  //
   234  // E.g.
   235  // /foo/bar -> ./zoo resolves to the absolute path /foo/zoo
   236  func resolveSymlinkTarget(symlink, target string) string {
   237  	if filepath.IsAbs(target) {
   238  		return target
   239  	}
   240  
   241  	return filepath.Join(filepath.Dir(symlink), target)
   242  }
   243  
   244  func (l *Lockfile) lockWith(pidpath string) error {
   245  	switch err := os.Symlink(pidpath, l.path); {
   246  	case err == nil:
   247  		return nil
   248  
   249  	case !os.IsExist(err):
   250  		// Some kind of system error.
   251  		return err
   252  
   253  	default:
   254  		// Symlink already exists.
   255  		switch err := l.checkLockfile(); err {
   256  		case errUnlocked:
   257  			return l.lockWith(pidpath)
   258  
   259  		case ErrInvalidPID, ErrProcessDead:
   260  			// Nuke the symlink and its target.
   261  			target, err := os.Readlink(l.path)
   262  			if os.IsNotExist(err) {
   263  				return l.lockWith(pidpath)
   264  			} else if err != nil {
   265  				// File might not be a symlink at all?
   266  				// Leave it alone, in case it's someone's
   267  				// legitimate file.
   268  				return err
   269  			}
   270  
   271  			absTarget := resolveSymlinkTarget(l.path, target)
   272  			// If it doesn't exist anymore, whatever. The symlink's
   273  			// existence is the actual lock.
   274  			if err := os.Remove(absTarget); !os.IsNotExist(err) && err != nil {
   275  				return err
   276  			}
   277  
   278  			if err := os.Remove(l.path); os.IsNotExist(err) {
   279  				return l.lockWith(pidpath)
   280  			} else if err != nil {
   281  				return err
   282  			}
   283  
   284  			// Retry making the symlink.
   285  			return l.lockWith(pidpath)
   286  
   287  		case ErrBusy, nil:
   288  			return err
   289  
   290  		default:
   291  			return err
   292  		}
   293  	}
   294  }