File: flock_test.go

package info (click to toggle)
snapd 2.72-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 80,412 kB
  • sloc: sh: 16,506; ansic: 16,211; python: 11,213; makefile: 1,919; exp: 190; awk: 58; xml: 22
file content (251 lines) | stat: -rw-r--r-- 8,132 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2017 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package osutil_test

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	. "gopkg.in/check.v1"

	"github.com/snapcore/snapd/osutil"
)

type flockSuite struct{}

var _ = Suite(&flockSuite{})

// Test that an existing lock file can be opened.
func (s *flockSuite) TestOpenExistingLockForReading(c *C) {
	fname := filepath.Join(c.MkDir(), "name")
	lock, err := osutil.OpenExistingLockForReading(fname)
	c.Assert(err, ErrorMatches, ".* no such file or directory")
	c.Assert(lock, IsNil)

	lock, err = osutil.NewFileLockWithMode(fname, 0644)
	c.Assert(err, IsNil)
	lock.Close()

	// Having created the lock above, we can now open it correctly.
	lock, err = osutil.OpenExistingLockForReading(fname)
	c.Assert(err, IsNil)
	defer lock.Close()

	// The lock file is read-only though.
	file := lock.File()
	defer file.Close()
	n, err := file.Write([]byte{1, 2, 3})
	// write(2) returns EBADF if the file descriptor is read only.
	c.Assert(err, ErrorMatches, ".* bad file descriptor")
	c.Assert(n, Equals, 0)
}

// Test that opening and closing a lock works as expected, and that the mode is right.
func (s *flockSuite) TestNewFileLockWithMode(c *C) {
	lock, err := osutil.NewFileLockWithMode(filepath.Join(c.MkDir(), "name"), 0644)
	c.Assert(err, IsNil)
	defer lock.Close()

	fi, err := os.Stat(lock.Path())
	c.Assert(err, IsNil)
	c.Assert(fi.Mode().Perm(), Equals, os.FileMode(0644))
}

// Test that opening and closing a lock works as expected.
func (s *flockSuite) TestNewFileLock(c *C) {
	lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name"))
	c.Assert(err, IsNil)
	defer lock.Close()

	fi, err := os.Stat(lock.Path())
	c.Assert(err, IsNil)
	c.Assert(fi.Mode().Perm(), Equals, os.FileMode(0600))
}

// Test that opening and closing a lock works as expected, and that the mode is right.
func (s *flockSuite) TestNewFileLockWithFile(c *C) {
	myfile, err := os.OpenFile(filepath.Join(c.MkDir(), "name"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	c.Assert(err, IsNil)

	lock := osutil.NewFileLockWithFile(myfile)
	defer lock.Close()

	fi, err := os.Stat(lock.Path())
	c.Assert(err, IsNil)
	c.Assert(fi.Mode().Perm(), Equals, os.FileMode(0600))
}

// Test that we can access the underlying open file.
func (s *flockSuite) TestFile(c *C) {
	fname := filepath.Join(c.MkDir(), "name")
	lock, err := osutil.NewFileLock(fname)
	c.Assert(err, IsNil)
	defer lock.Close()

	f := lock.File()
	c.Assert(f, NotNil)
	c.Check(f.Name(), Equals, fname)
}

func flockSupportsConflictExitCodeSwitch(c *C) bool {
	output, err := exec.Command("flock", "--help").CombinedOutput()
	c.Assert(err, IsNil)
	return bytes.Contains(output, []byte("--conflict-exit-code"))
}

// Test that Lock and Unlock work as expected.
func (s *flockSuite) TestLockUnlockWorks(c *C) {
	if !flockSupportsConflictExitCodeSwitch(c) {
		c.Skip("flock too old for this test")
	}

	lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name"))
	c.Assert(err, IsNil)
	defer lock.Close()

	// Run a flock command in another process, it should succeed because it can
	// lock the lock as we didn't do it yet.
	cmd := exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true")
	c.Assert(cmd.Run(), IsNil)

	// Lock the lock.
	c.Assert(lock.Lock(), IsNil)

	// Run a flock command in another process, it should fail with the distinct
	// error code because we hold the lock already and we asked it not to block.
	cmd = exec.Command("flock", "--exclusive", "--nonblock",
		"--conflict-exit-code", "2", lock.Path(), "true")
	c.Assert(cmd.Run(), ErrorMatches, "exit status 2")

	// Unlock the lock.
	c.Assert(lock.Unlock(), IsNil)

	// Run a flock command in another process, it should succeed because it can
	// grab the lock again now.
	cmd = exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true")
	c.Assert(cmd.Run(), IsNil)
}

// Test that ReadLock and Unlock work as expected.
func (s *flockSuite) TestReadLockUnlockWorks(c *C) {
	if !flockSupportsConflictExitCodeSwitch(c) {
		c.Skip("flock too old for this test")
	}

	lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name"))
	c.Assert(err, IsNil)
	defer lock.Close()

	// Run a flock command in another process, it should succeed because it can
	// lock the lock as we didn't do it yet.
	cmd := exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true")
	c.Assert(cmd.Run(), IsNil)

	// Grab a shared lock.
	c.Assert(lock.ReadLock(), IsNil)

	// Run a flock command in another process, it should fail with the distinct
	// error code because we hold a shared lock already and we asked it not to block.
	cmd = exec.Command("flock", "--exclusive", "--nonblock",
		"--conflict-exit-code", "2", lock.Path(), "true")
	c.Assert(cmd.Run(), ErrorMatches, "exit status 2")

	// Run a flock command in another process, it should succeed because we
	// hold a shared lock and those do not prevent others from acquiring a
	// shared lock.
	cmd = exec.Command("flock", "--shared", "--nonblock",
		"--conflict-exit-code", "2", lock.Path(), "true")
	c.Assert(cmd.Run(), IsNil)

	// Unlock the lock.
	c.Assert(lock.Unlock(), IsNil)

	// Run a flock command in another process, it should succeed because it can
	// grab the lock again now.
	cmd = exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true")
	c.Assert(cmd.Run(), IsNil)
}

// Test that locking a locked lock does nothing.
func (s *flockSuite) TestLockLocked(c *C) {
	lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name"))
	c.Assert(err, IsNil)
	defer lock.Close()

	// NOTE: technically this replaces the lock type but we only use LOCK_EX.
	c.Assert(lock.Lock(), IsNil)
	c.Assert(lock.Lock(), IsNil)
}

// Test that unlocking an unlocked lock does nothing.
func (s *flockSuite) TestUnlockUnlocked(c *C) {
	lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name"))
	c.Assert(err, IsNil)
	defer lock.Close()

	c.Assert(lock.Unlock(), IsNil)
}

// Test that locking or unlocking a closed lock fails.
func (s *flockSuite) TestUsingClosedLock(c *C) {
	lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name"))
	c.Assert(err, IsNil)
	lock.Close()

	c.Assert(lock.Lock(), ErrorMatches, "bad file descriptor")
	c.Assert(lock.Unlock(), ErrorMatches, "bad file descriptor")
}

// Test that non-blocking locking reports error on pre-acquired lock.
func (s *flockSuite) TestLockUnlockNonblockingWorks(c *C) {
	// Use the "flock" command to grab a lock for 9999 seconds in another process.
	lockPath := filepath.Join(c.MkDir(), "lock")
	sleeperKillerPath := filepath.Join(c.MkDir(), "pid")
	// we can't use --no-fork because we still support 14.04
	cmd := exec.Command("flock", "--exclusive", lockPath, "-c", fmt.Sprintf(`echo "kill $$" > %s && exec sleep 30`, sleeperKillerPath))

	// flock uses the env variable 'SHELL' to run the passed in command. a non-posix
	// shell will not understand $$. we can force flock to use its default by unsetting
	// the variable
	cmd.Env = append(cmd.Env, "SHELL=")

	c.Assert(cmd.Start(), IsNil)
	defer func() { exec.Command("/bin/sh", sleeperKillerPath).Run() }()

	// Give flock some chance to create the lock file.
	for i := 0; i < 10; i++ {
		if osutil.FileExists(lockPath) {
			break
		}
		time.Sleep(time.Millisecond * 300)
	}

	// Try to acquire the same lock file and see that it is busy.
	lock, err := osutil.NewFileLock(lockPath)
	c.Assert(err, IsNil)
	c.Assert(lock, NotNil)
	defer lock.Close()

	c.Assert(lock.TryLock(), Equals, osutil.ErrAlreadyLocked)
}