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
5// +build plan9
6
7package lockedfile
8
9import (
10	"math/rand"
11	"os"
12	"strings"
13	"time"
14)
15
16// Opening an exclusive-use file returns an error.
17// The expected error strings are:
18//
19//  - "open/create -- file is locked" (cwfs, kfs)
20//  - "exclusive lock" (fossil)
21//  - "exclusive use file already open" (ramfs)
22var lockedErrStrings = [...]string{
23	"file is locked",
24	"exclusive lock",
25	"exclusive use file already open",
26}
27
28// Even though plan9 doesn't support the Lock/RLock/Unlock functions to
29// manipulate already-open files, IsLocked is still meaningful: os.OpenFile
30// itself may return errors that indicate that a file with the ModeExclusive bit
31// set is already open.
32func isLocked(err error) bool {
33	s := err.Error()
34
35	for _, frag := range lockedErrStrings {
36		if strings.Contains(s, frag) {
37			return true
38		}
39	}
40
41	return false
42}
43
44func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
45	// Plan 9 uses a mode bit instead of explicit lock/unlock syscalls.
46	//
47	// Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open
48	// for I/O by only one fid at a time across all clients of the server. If a
49	// second open is attempted, it draws an error.”
50	//
51	// So we can try to open a locked file, but if it fails we're on our own to
52	// figure out when it becomes available. We'll use exponential backoff with
53	// some jitter and an arbitrary limit of 500ms.
54
55	// If the file was unpacked or created by some other program, it might not
56	// have the ModeExclusive bit set. Set it before we call OpenFile, so that we
57	// can be confident that a successful OpenFile implies exclusive use.
58	if fi, err := os.Stat(name); err == nil {
59		if fi.Mode()&os.ModeExclusive == 0 {
60			if err := os.Chmod(name, fi.Mode()|os.ModeExclusive); err != nil {
61				return nil, err
62			}
63		}
64	} else if !os.IsNotExist(err) {
65		return nil, err
66	}
67
68	nextSleep := 1 * time.Millisecond
69	const maxSleep = 500 * time.Millisecond
70	for {
71		f, err := os.OpenFile(name, flag, perm|os.ModeExclusive)
72		if err == nil {
73			return f, nil
74		}
75
76		if !isLocked(err) {
77			return nil, err
78		}
79
80		time.Sleep(nextSleep)
81
82		nextSleep += nextSleep
83		if nextSleep > maxSleep {
84			nextSleep = maxSleep
85		}
86		// Apply 10% jitter to avoid synchronizing collisions.
87		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
88	}
89}
90
91func closeFile(f *os.File) error {
92	return f.Close()
93}
94