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