136 lines
3.1 KiB
Go
136 lines
3.1 KiB
Go
// Handle pid file based locking.
|
|
package lockfile
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type Lockfile string
|
|
|
|
var (
|
|
ErrBusy = errors.New("Locked by other process") // If you get this, retry after a short sleep might help
|
|
ErrNeedAbsPath = errors.New("Lockfiles must be given as absolute path names")
|
|
ErrInvalidPid = errors.New("Lockfile contains invalid pid for system")
|
|
ErrDeadOwner = errors.New("Lockfile contains pid of process not existent on this system anymore")
|
|
)
|
|
|
|
// Describe a new filename located at path. It is expected to be an absolute path
|
|
func New(path string) (Lockfile, error) {
|
|
if !filepath.IsAbs(path) {
|
|
return Lockfile(""), ErrNeedAbsPath
|
|
}
|
|
return Lockfile(path), nil
|
|
}
|
|
|
|
// Who owns the lockfile?
|
|
func (l Lockfile) GetOwner() (*os.Process, error) {
|
|
name := string(l)
|
|
|
|
// Ok, see, if we have a stale lockfile here
|
|
content, err := ioutil.ReadFile(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pid int
|
|
_, err = fmt.Sscanln(string(content), &pid)
|
|
if err != nil {
|
|
return nil, ErrInvalidPid
|
|
}
|
|
|
|
// try hard for pids. If no pid, the lockfile is junk anyway and we delete it.
|
|
if pid > 0 {
|
|
running, err := isRunning(pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if running {
|
|
proc, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return proc, nil
|
|
} else {
|
|
return nil, ErrDeadOwner
|
|
}
|
|
|
|
} else {
|
|
return nil, ErrInvalidPid
|
|
}
|
|
panic("Not reached")
|
|
}
|
|
|
|
// Try to get Lockfile lock. Returns nil, if successful and and error describing the reason, it didn't work out.
|
|
// Please note, that existing lockfiles containing pids of dead processes and lockfiles containing no pid at all
|
|
// are deleted.
|
|
func (l Lockfile) TryLock() error {
|
|
name := string(l)
|
|
|
|
// This has been checked by New already. If we trigger here,
|
|
// the caller didn't use New and re-implemented it's functionality badly.
|
|
// So panic, that he might find this easily during testing.
|
|
if !filepath.IsAbs(string(name)) {
|
|
panic(ErrNeedAbsPath)
|
|
}
|
|
|
|
tmplock, err := ioutil.TempFile(filepath.Dir(name), "")
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
defer tmplock.Close()
|
|
defer os.Remove(tmplock.Name())
|
|
}
|
|
|
|
_, err = tmplock.WriteString(fmt.Sprintf("%d\n", os.Getpid()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// return value intentionally ignored, as ignoring it is part of the algorithm
|
|
_ = os.Link(tmplock.Name(), name)
|
|
|
|
fiTmp, err := os.Lstat(tmplock.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fiLock, err := os.Lstat(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Success
|
|
if os.SameFile(fiTmp, fiLock) {
|
|
return nil
|
|
}
|
|
|
|
_, err = l.GetOwner()
|
|
switch err {
|
|
default:
|
|
// Other errors -> defensively fail and let caller handle this
|
|
return err
|
|
case nil:
|
|
return ErrBusy
|
|
case ErrDeadOwner, ErrInvalidPid:
|
|
// cases we can fix below
|
|
}
|
|
|
|
// clean stale/invalid lockfile
|
|
err = os.Remove(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// now that we cleaned up the stale lockfile, let's recurse
|
|
return l.TryLock()
|
|
}
|
|
|
|
// Release a lock again. Returns any error that happend during release of lock.
|
|
func (l Lockfile) Unlock() error {
|
|
return os.Remove(string(l))
|
|
}
|