1// Copyright 2018 The Go 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
5package lockedfile
6
7import (
8	"fmt"
9	"os"
10	"sync"
11)
12
13// A Mutex provides mutual exclusion within and across processes by locking a
14// well-known file. Such a file generally guards some other part of the
15// filesystem: for example, a Mutex file in a directory might guard access to
16// the entire tree rooted in that directory.
17//
18// Mutex does not implement sync.Locker: unlike a sync.Mutex, a lockedfile.Mutex
19// can fail to lock (e.g. if there is a permission error in the filesystem).
20//
21// Like a sync.Mutex, a Mutex may be included as a field of a larger struct but
22// must not be copied after first use. The Path field must be set before first
23// use and must not be change thereafter.
24type Mutex struct {
25	Path string     // The path to the well-known lock file. Must be non-empty.
26	mu   sync.Mutex // A redundant mutex. The race detector doesn't know about file locking, so in tests we may need to lock something that it understands.
27}
28
29// MutexAt returns a new Mutex with Path set to the given non-empty path.
30func MutexAt(path string) *Mutex {
31	if path == "" {
32		panic("lockedfile.MutexAt: path must be non-empty")
33	}
34	return &Mutex{Path: path}
35}
36
37func (mu *Mutex) String() string {
38	return fmt.Sprintf("lockedfile.Mutex(%s)", mu.Path)
39}
40
41// Lock attempts to lock the Mutex.
42//
43// If successful, Lock returns a non-nil unlock function: it is provided as a
44// return-value instead of a separate method to remind the caller to check the
45// accompanying error. (See https://golang.org/issue/20803.)
46func (mu *Mutex) Lock() (unlock func(), err error) {
47	if mu.Path == "" {
48		panic("lockedfile.Mutex: missing Path during Lock")
49	}
50
51	// We could use either O_RDWR or O_WRONLY here. If we choose O_RDWR and the
52	// file at mu.Path is write-only, the call to OpenFile will fail with a
53	// permission error. That's actually what we want: if we add an RLock method
54	// in the future, it should call OpenFile with O_RDONLY and will require the
55	// files must be readable, so we should not let the caller make any
56	// assumptions about Mutex working with write-only files.
57	f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0666)
58	if err != nil {
59		return nil, err
60	}
61	mu.mu.Lock()
62
63	return func() {
64		mu.mu.Unlock()
65		f.Close()
66	}, nil
67}
68