1// Package lockfile handles pid file based locking. 2// While a sync.Mutex helps against concurrency issues within a single process, 3// this package is designed to help against concurrency issues between cooperating processes 4// or serializing multiple invocations of the same process. 5package lockfile 6 7import ( 8 "errors" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14) 15 16// Lockfile is a pid file which can be locked 17type Lockfile string 18 19// TemporaryError is a type of error where a retry after a random amount of sleep should help to mitigate it. 20type TemporaryError string 21 22func (t TemporaryError) Error() string { return string(t) } 23 24// Temporary returns always true. 25// It exists, so you can detect it via 26// if te, ok := err.(interface{ Temporary() bool }); ok { 27// fmt.Println("I am a temporay error situation, so wait and retry") 28// } 29func (t TemporaryError) Temporary() bool { return true } 30 31// Various errors returned by this package 32var ( 33 ErrBusy = TemporaryError("Locked by other process") // If you get this, retry after a short sleep might help 34 ErrNotExist = TemporaryError("Lockfile created, but doesn't exist") // If you get this, retry after a short sleep might help 35 ErrNeedAbsPath = errors.New("Lockfiles must be given as absolute path names") 36 ErrInvalidPid = errors.New("Lockfile contains invalid pid for system") 37 ErrDeadOwner = errors.New("Lockfile contains pid of process not existent on this system anymore") 38 ErrRogueDeletion = errors.New("Lockfile owned by me has been removed unexpectedly") 39) 40 41// New describes a new filename located at the given absolute path. 42func New(path string) (Lockfile, error) { 43 if !filepath.IsAbs(path) { 44 return Lockfile(""), ErrNeedAbsPath 45 } 46 return Lockfile(path), nil 47} 48 49// GetOwner returns who owns the lockfile. 50func (l Lockfile) GetOwner() (*os.Process, error) { 51 name := string(l) 52 53 // Ok, see, if we have a stale lockfile here 54 content, err := ioutil.ReadFile(name) 55 if err != nil { 56 return nil, err 57 } 58 59 // try hard for pids. If no pid, the lockfile is junk anyway and we delete it. 60 pid, err := scanPidLine(content) 61 if err != nil { 62 return nil, err 63 } 64 running, err := isRunning(pid) 65 if err != nil { 66 return nil, err 67 } 68 69 if running { 70 proc, err := os.FindProcess(pid) 71 if err != nil { 72 return nil, err 73 } 74 return proc, nil 75 } 76 return nil, ErrDeadOwner 77 78} 79 80// TryLock tries to own the lock. 81// It Returns nil, if successful and and error describing the reason, it didn't work out. 82// Please note, that existing lockfiles containing pids of dead processes 83// and lockfiles containing no pid at all are simply deleted. 84func (l Lockfile) TryLock() error { 85 name := string(l) 86 87 // This has been checked by New already. If we trigger here, 88 // the caller didn't use New and re-implemented it's functionality badly. 89 // So panic, that he might find this easily during testing. 90 if !filepath.IsAbs(name) { 91 panic(ErrNeedAbsPath) 92 } 93 94 tmplock, err := ioutil.TempFile(filepath.Dir(name), "") 95 if err != nil { 96 return err 97 } 98 99 cleanup := func() { 100 _ = tmplock.Close() 101 _ = os.Remove(tmplock.Name()) 102 } 103 defer cleanup() 104 105 if err := writePidLine(tmplock, os.Getpid()); err != nil { 106 return err 107 } 108 109 // return value intentionally ignored, as ignoring it is part of the algorithm 110 _ = os.Link(tmplock.Name(), name) 111 112 fiTmp, err := os.Lstat(tmplock.Name()) 113 if err != nil { 114 return err 115 } 116 fiLock, err := os.Lstat(name) 117 if err != nil { 118 // tell user that a retry would be a good idea 119 if os.IsNotExist(err) { 120 return ErrNotExist 121 } 122 return err 123 } 124 125 // Success 126 if os.SameFile(fiTmp, fiLock) { 127 return nil 128 } 129 130 _, err = l.GetOwner() 131 switch err { 132 default: 133 // Other errors -> defensively fail and let caller handle this 134 return err 135 case nil: 136 // This fork differs from the upstream repository in this line. We do 137 // not want a process to obtain a lock if this lock is already help by 138 // the same process. Therefore, we always return ErrBusy if the lockfile 139 // contains a non-dead PID. 140 return ErrBusy 141 case ErrDeadOwner, ErrInvalidPid: 142 // cases we can fix below 143 } 144 145 // clean stale/invalid lockfile 146 err = os.Remove(name) 147 if err != nil { 148 // If it doesn't exist, then it doesn't matter who removed it. 149 if !os.IsNotExist(err) { 150 return err 151 } 152 } 153 154 // now that the stale lockfile is gone, let's recurse 155 return l.TryLock() 156} 157 158// Unlock a lock again, if we owned it. Returns any error that happend during release of lock. 159func (l Lockfile) Unlock() error { 160 proc, err := l.GetOwner() 161 switch err { 162 case ErrInvalidPid, ErrDeadOwner: 163 return ErrRogueDeletion 164 case nil: 165 if proc.Pid == os.Getpid() { 166 // we really own it, so let's remove it. 167 return os.Remove(string(l)) 168 } 169 // Not owned by me, so don't delete it. 170 return ErrRogueDeletion 171 default: 172 // This is an application error or system error. 173 // So give a better error for logging here. 174 if os.IsNotExist(err) { 175 return ErrRogueDeletion 176 } 177 // Other errors -> defensively fail and let caller handle this 178 return err 179 } 180} 181 182func writePidLine(w io.Writer, pid int) error { 183 _, err := io.WriteString(w, fmt.Sprintf("%d\n", pid)) 184 return err 185} 186 187func scanPidLine(content []byte) (int, error) { 188 if len(content) == 0 { 189 return 0, ErrInvalidPid 190 } 191 192 var pid int 193 if _, err := fmt.Sscanln(string(content), &pid); err != nil { 194 return 0, ErrInvalidPid 195 } 196 197 if pid <= 0 { 198 return 0, ErrInvalidPid 199 } 200 return pid, nil 201} 202