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//go:build !js && !plan9 && !wasip1
6
7package filelock_test
8
9import (
10	"fmt"
11	"internal/testenv"
12	"os"
13	"path/filepath"
14	"runtime"
15	"testing"
16	"time"
17
18	"cmd/go/internal/lockedfile/internal/filelock"
19)
20
21func lock(t *testing.T, f *os.File) {
22	t.Helper()
23	err := filelock.Lock(f)
24	t.Logf("Lock(fd %d) = %v", f.Fd(), err)
25	if err != nil {
26		t.Fail()
27	}
28}
29
30func rLock(t *testing.T, f *os.File) {
31	t.Helper()
32	err := filelock.RLock(f)
33	t.Logf("RLock(fd %d) = %v", f.Fd(), err)
34	if err != nil {
35		t.Fail()
36	}
37}
38
39func unlock(t *testing.T, f *os.File) {
40	t.Helper()
41	err := filelock.Unlock(f)
42	t.Logf("Unlock(fd %d) = %v", f.Fd(), err)
43	if err != nil {
44		t.Fail()
45	}
46}
47
48func mustTempFile(t *testing.T) (f *os.File, remove func()) {
49	t.Helper()
50
51	base := filepath.Base(t.Name())
52	f, err := os.CreateTemp("", base)
53	if err != nil {
54		t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
55	}
56	t.Logf("fd %d = %s", f.Fd(), f.Name())
57
58	return f, func() {
59		f.Close()
60		os.Remove(f.Name())
61	}
62}
63
64func mustOpen(t *testing.T, name string) *os.File {
65	t.Helper()
66
67	f, err := os.OpenFile(name, os.O_RDWR, 0)
68	if err != nil {
69		t.Fatalf("os.Open(%q) = %v", name, err)
70	}
71
72	t.Logf("fd %d = os.Open(%q)", f.Fd(), name)
73	return f
74}
75
76const (
77	quiescent            = 10 * time.Millisecond
78	probablyStillBlocked = 10 * time.Second
79)
80
81func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) {
82	t.Helper()
83
84	desc := fmt.Sprintf("%s(fd %d)", op, f.Fd())
85
86	done := make(chan struct{})
87	go func() {
88		t.Helper()
89		switch op {
90		case "Lock":
91			lock(t, f)
92		case "RLock":
93			rLock(t, f)
94		default:
95			panic("invalid op: " + op)
96		}
97		close(done)
98	}()
99
100	select {
101	case <-done:
102		t.Fatalf("%s unexpectedly did not block", desc)
103		return nil
104
105	case <-time.After(quiescent):
106		t.Logf("%s is blocked (as expected)", desc)
107		return func(t *testing.T) {
108			t.Helper()
109			select {
110			case <-time.After(probablyStillBlocked):
111				t.Fatalf("%s is unexpectedly still blocked", desc)
112			case <-done:
113			}
114		}
115	}
116}
117
118func TestLockExcludesLock(t *testing.T) {
119	t.Parallel()
120
121	f, remove := mustTempFile(t)
122	defer remove()
123
124	other := mustOpen(t, f.Name())
125	defer other.Close()
126
127	lock(t, f)
128	lockOther := mustBlock(t, "Lock", other)
129	unlock(t, f)
130	lockOther(t)
131	unlock(t, other)
132}
133
134func TestLockExcludesRLock(t *testing.T) {
135	t.Parallel()
136
137	f, remove := mustTempFile(t)
138	defer remove()
139
140	other := mustOpen(t, f.Name())
141	defer other.Close()
142
143	lock(t, f)
144	rLockOther := mustBlock(t, "RLock", other)
145	unlock(t, f)
146	rLockOther(t)
147	unlock(t, other)
148}
149
150func TestRLockExcludesOnlyLock(t *testing.T) {
151	t.Parallel()
152
153	f, remove := mustTempFile(t)
154	defer remove()
155	rLock(t, f)
156
157	f2 := mustOpen(t, f.Name())
158	defer f2.Close()
159
160	doUnlockTF := false
161	switch runtime.GOOS {
162	case "aix", "solaris":
163		// When using POSIX locks (as on Solaris), we can't safely read-lock the
164		// same inode through two different descriptors at the same time: when the
165		// first descriptor is closed, the second descriptor would still be open but
166		// silently unlocked. So a second RLock must block instead of proceeding.
167		lockF2 := mustBlock(t, "RLock", f2)
168		unlock(t, f)
169		lockF2(t)
170	default:
171		rLock(t, f2)
172		doUnlockTF = true
173	}
174
175	other := mustOpen(t, f.Name())
176	defer other.Close()
177	lockOther := mustBlock(t, "Lock", other)
178
179	unlock(t, f2)
180	if doUnlockTF {
181		unlock(t, f)
182	}
183	lockOther(t)
184	unlock(t, other)
185}
186
187func TestLockNotDroppedByExecCommand(t *testing.T) {
188	testenv.MustHaveExec(t)
189
190	f, remove := mustTempFile(t)
191	defer remove()
192
193	lock(t, f)
194
195	other := mustOpen(t, f.Name())
196	defer other.Close()
197
198	// Some kinds of file locks are dropped when a duplicated or forked file
199	// descriptor is unlocked. Double-check that the approach used by os/exec does
200	// not accidentally drop locks.
201	cmd := testenv.Command(t, os.Args[0], "-test.run=^$")
202	if err := cmd.Run(); err != nil {
203		t.Fatalf("exec failed: %v", err)
204	}
205
206	lockOther := mustBlock(t, "Lock", other)
207	unlock(t, f)
208	lockOther(t)
209	unlock(t, other)
210}
211