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// js and wasip1 do not support inter-process file locking.
6//
7//go:build !js && !wasip1
8
9package lockedfile_test
10
11import (
12	"fmt"
13	"internal/testenv"
14	"os"
15	"path/filepath"
16	"testing"
17	"time"
18
19	"cmd/go/internal/lockedfile"
20)
21
22func mustTempDir(t *testing.T) (dir string, remove func()) {
23	t.Helper()
24
25	dir, err := os.MkdirTemp("", filepath.Base(t.Name()))
26	if err != nil {
27		t.Fatal(err)
28	}
29	return dir, func() { os.RemoveAll(dir) }
30}
31
32const (
33	quiescent            = 10 * time.Millisecond
34	probablyStillBlocked = 10 * time.Second
35)
36
37func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) {
38	t.Helper()
39
40	done := make(chan struct{})
41	go func() {
42		f()
43		close(done)
44	}()
45
46	timer := time.NewTimer(quiescent)
47	defer timer.Stop()
48	select {
49	case <-done:
50		t.Fatalf("%s unexpectedly did not block", desc)
51	case <-timer.C:
52	}
53
54	return func(t *testing.T) {
55		logTimer := time.NewTimer(quiescent)
56		defer logTimer.Stop()
57
58		select {
59		case <-logTimer.C:
60			// We expect the operation to have unblocked by now,
61			// but maybe it's just slow. Write to the test log
62			// in case the test times out, but don't fail it.
63			t.Helper()
64			t.Logf("%s is unexpectedly still blocked after %v", desc, quiescent)
65
66			// Wait for the operation to actually complete, no matter how long it
67			// takes. If the test has deadlocked, this will cause the test to time out
68			// and dump goroutines.
69			<-done
70
71		case <-done:
72		}
73	}
74}
75
76func TestMutexExcludes(t *testing.T) {
77	t.Parallel()
78
79	dir, remove := mustTempDir(t)
80	defer remove()
81
82	path := filepath.Join(dir, "lock")
83
84	mu := lockedfile.MutexAt(path)
85	t.Logf("mu := MutexAt(_)")
86
87	unlock, err := mu.Lock()
88	if err != nil {
89		t.Fatalf("mu.Lock: %v", err)
90	}
91	t.Logf("unlock, _  := mu.Lock()")
92
93	mu2 := lockedfile.MutexAt(mu.Path)
94	t.Logf("mu2 := MutexAt(mu.Path)")
95
96	wait := mustBlock(t, "mu2.Lock()", func() {
97		unlock2, err := mu2.Lock()
98		if err != nil {
99			t.Errorf("mu2.Lock: %v", err)
100			return
101		}
102		t.Logf("unlock2, _ := mu2.Lock()")
103		t.Logf("unlock2()")
104		unlock2()
105	})
106
107	t.Logf("unlock()")
108	unlock()
109	wait(t)
110}
111
112func TestReadWaitsForLock(t *testing.T) {
113	t.Parallel()
114
115	dir, remove := mustTempDir(t)
116	defer remove()
117
118	path := filepath.Join(dir, "timestamp.txt")
119
120	f, err := lockedfile.Create(path)
121	if err != nil {
122		t.Fatalf("Create: %v", err)
123	}
124	defer f.Close()
125
126	const (
127		part1 = "part 1\n"
128		part2 = "part 2\n"
129	)
130	_, err = f.WriteString(part1)
131	if err != nil {
132		t.Fatalf("WriteString: %v", err)
133	}
134	t.Logf("WriteString(%q) = <nil>", part1)
135
136	wait := mustBlock(t, "Read", func() {
137		b, err := lockedfile.Read(path)
138		if err != nil {
139			t.Errorf("Read: %v", err)
140			return
141		}
142
143		const want = part1 + part2
144		got := string(b)
145		if got == want {
146			t.Logf("Read(_) = %q", got)
147		} else {
148			t.Errorf("Read(_) = %q, _; want %q", got, want)
149		}
150	})
151
152	_, err = f.WriteString(part2)
153	if err != nil {
154		t.Errorf("WriteString: %v", err)
155	} else {
156		t.Logf("WriteString(%q) = <nil>", part2)
157	}
158	f.Close()
159
160	wait(t)
161}
162
163func TestCanLockExistingFile(t *testing.T) {
164	t.Parallel()
165
166	dir, remove := mustTempDir(t)
167	defer remove()
168	path := filepath.Join(dir, "existing.txt")
169
170	if err := os.WriteFile(path, []byte("ok"), 0777); err != nil {
171		t.Fatalf("os.WriteFile: %v", err)
172	}
173
174	f, err := lockedfile.Edit(path)
175	if err != nil {
176		t.Fatalf("first Edit: %v", err)
177	}
178
179	wait := mustBlock(t, "Edit", func() {
180		other, err := lockedfile.Edit(path)
181		if err != nil {
182			t.Errorf("second Edit: %v", err)
183		}
184		other.Close()
185	})
186
187	f.Close()
188	wait(t)
189}
190
191// TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in
192// https://golang.org/issue/32817 no longer occurs.
193func TestSpuriousEDEADLK(t *testing.T) {
194	// 	P.1 locks file A.
195	// 	Q.3 locks file B.
196	// 	Q.3 blocks on file A.
197	// 	P.2 blocks on file B. (Spurious EDEADLK occurs here.)
198	// 	P.1 unlocks file A.
199	// 	Q.3 unblocks and locks file A.
200	// 	Q.3 unlocks files A and B.
201	// 	P.2 unblocks and locks file B.
202	// 	P.2 unlocks file B.
203
204	testenv.MustHaveExec(t)
205
206	dirVar := t.Name() + "DIR"
207
208	if dir := os.Getenv(dirVar); dir != "" {
209		// Q.3 locks file B.
210		b, err := lockedfile.Edit(filepath.Join(dir, "B"))
211		if err != nil {
212			t.Fatal(err)
213		}
214		defer b.Close()
215
216		if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil {
217			t.Fatal(err)
218		}
219
220		// Q.3 blocks on file A.
221		a, err := lockedfile.Edit(filepath.Join(dir, "A"))
222		// Q.3 unblocks and locks file A.
223		if err != nil {
224			t.Fatal(err)
225		}
226		defer a.Close()
227
228		// Q.3 unlocks files A and B.
229		return
230	}
231
232	dir, remove := mustTempDir(t)
233	defer remove()
234
235	// P.1 locks file A.
236	a, err := lockedfile.Edit(filepath.Join(dir, "A"))
237	if err != nil {
238		t.Fatal(err)
239	}
240
241	cmd := testenv.Command(t, os.Args[0], "-test.run=^"+t.Name()+"$")
242	cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir))
243
244	qDone := make(chan struct{})
245	waitQ := mustBlock(t, "Edit A and B in subprocess", func() {
246		out, err := cmd.CombinedOutput()
247		if err != nil {
248			t.Errorf("%v:\n%s", err, out)
249		}
250		close(qDone)
251	})
252
253	// Wait until process Q has either failed or locked file B.
254	// Otherwise, P.2 might not block on file B as intended.
255locked:
256	for {
257		if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) {
258			break locked
259		}
260		timer := time.NewTimer(1 * time.Millisecond)
261		select {
262		case <-qDone:
263			timer.Stop()
264			break locked
265		case <-timer.C:
266		}
267	}
268
269	waitP2 := mustBlock(t, "Edit B", func() {
270		// P.2 blocks on file B. (Spurious EDEADLK occurs here.)
271		b, err := lockedfile.Edit(filepath.Join(dir, "B"))
272		// P.2 unblocks and locks file B.
273		if err != nil {
274			t.Error(err)
275			return
276		}
277		// P.2 unlocks file B.
278		b.Close()
279	})
280
281	// P.1 unlocks file A.
282	a.Close()
283
284	waitQ(t)
285	waitP2(t)
286}
287