github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/flock_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package osutil_test 21 22 import ( 23 "bytes" 24 "fmt" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "time" 29 30 . "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/osutil" 33 ) 34 35 type flockSuite struct{} 36 37 var _ = Suite(&flockSuite{}) 38 39 // Test that an existing lock file can be opened. 40 func (s *flockSuite) TestOpenExistingLockForReading(c *C) { 41 fname := filepath.Join(c.MkDir(), "name") 42 lock, err := osutil.OpenExistingLockForReading(fname) 43 c.Assert(err, ErrorMatches, ".* no such file or directory") 44 c.Assert(lock, IsNil) 45 46 lock, err = osutil.NewFileLockWithMode(fname, 0644) 47 c.Assert(err, IsNil) 48 lock.Close() 49 50 // Having created the lock above, we can now open it correctly. 51 lock, err = osutil.OpenExistingLockForReading(fname) 52 c.Assert(err, IsNil) 53 defer lock.Close() 54 55 // The lock file is read-only though. 56 file := lock.File() 57 defer file.Close() 58 n, err := file.Write([]byte{1, 2, 3}) 59 // write(2) returns EBADF if the file descriptor is read only. 60 c.Assert(err, ErrorMatches, ".* bad file descriptor") 61 c.Assert(n, Equals, 0) 62 } 63 64 // Test that opening and closing a lock works as expected, and that the mode is right. 65 func (s *flockSuite) TestNewFileLockWithMode(c *C) { 66 lock, err := osutil.NewFileLockWithMode(filepath.Join(c.MkDir(), "name"), 0644) 67 c.Assert(err, IsNil) 68 defer lock.Close() 69 70 fi, err := os.Stat(lock.Path()) 71 c.Assert(err, IsNil) 72 c.Assert(fi.Mode().Perm(), Equals, os.FileMode(0644)) 73 } 74 75 // Test that opening and closing a lock works as expected. 76 func (s *flockSuite) TestNewFileLock(c *C) { 77 lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name")) 78 c.Assert(err, IsNil) 79 defer lock.Close() 80 81 fi, err := os.Stat(lock.Path()) 82 c.Assert(err, IsNil) 83 c.Assert(fi.Mode().Perm(), Equals, os.FileMode(0600)) 84 } 85 86 // Test that we can access the underlying open file. 87 func (s *flockSuite) TestFile(c *C) { 88 fname := filepath.Join(c.MkDir(), "name") 89 lock, err := osutil.NewFileLock(fname) 90 c.Assert(err, IsNil) 91 defer lock.Close() 92 93 f := lock.File() 94 c.Assert(f, NotNil) 95 c.Check(f.Name(), Equals, fname) 96 } 97 98 func flockSupportsConflictExitCodeSwitch(c *C) bool { 99 output, err := exec.Command("flock", "--help").CombinedOutput() 100 c.Assert(err, IsNil) 101 return bytes.Contains(output, []byte("--conflict-exit-code")) 102 } 103 104 // Test that Lock and Unlock work as expected. 105 func (s *flockSuite) TestLockUnlockWorks(c *C) { 106 if os.Getenv("TRAVIS_BUILD_NUMBER") != "" { 107 c.Skip("Cannot use this under travis") 108 return 109 } 110 if !flockSupportsConflictExitCodeSwitch(c) { 111 c.Skip("flock too old for this test") 112 } 113 114 lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name")) 115 c.Assert(err, IsNil) 116 defer lock.Close() 117 118 // Run a flock command in another process, it should succeed because it can 119 // lock the lock as we didn't do it yet. 120 cmd := exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true") 121 c.Assert(cmd.Run(), IsNil) 122 123 // Lock the lock. 124 c.Assert(lock.Lock(), IsNil) 125 126 // Run a flock command in another process, it should fail with the distinct 127 // error code because we hold the lock already and we asked it not to block. 128 cmd = exec.Command("flock", "--exclusive", "--nonblock", 129 "--conflict-exit-code", "2", lock.Path(), "true") 130 c.Assert(cmd.Run(), ErrorMatches, "exit status 2") 131 132 // Unlock the lock. 133 c.Assert(lock.Unlock(), IsNil) 134 135 // Run a flock command in another process, it should succeed because it can 136 // grab the lock again now. 137 cmd = exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true") 138 c.Assert(cmd.Run(), IsNil) 139 } 140 141 // Test that ReadLock and Unlock work as expected. 142 func (s *flockSuite) TestReadLockUnlockWorks(c *C) { 143 if os.Getenv("TRAVIS_BUILD_NUMBER") != "" { 144 c.Skip("Cannot use this under travis") 145 return 146 } 147 if !flockSupportsConflictExitCodeSwitch(c) { 148 c.Skip("flock too old for this test") 149 } 150 151 lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name")) 152 c.Assert(err, IsNil) 153 defer lock.Close() 154 155 // Run a flock command in another process, it should succeed because it can 156 // lock the lock as we didn't do it yet. 157 cmd := exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true") 158 c.Assert(cmd.Run(), IsNil) 159 160 // Grab a shared lock. 161 c.Assert(lock.ReadLock(), IsNil) 162 163 // Run a flock command in another process, it should fail with the distinct 164 // error code because we hold a shared lock already and we asked it not to block. 165 cmd = exec.Command("flock", "--exclusive", "--nonblock", 166 "--conflict-exit-code", "2", lock.Path(), "true") 167 c.Assert(cmd.Run(), ErrorMatches, "exit status 2") 168 169 // Run a flock command in another process, it should succeed because we 170 // hold a shared lock and those do not prevent others from acquiring a 171 // shared lock. 172 cmd = exec.Command("flock", "--shared", "--nonblock", 173 "--conflict-exit-code", "2", lock.Path(), "true") 174 c.Assert(cmd.Run(), IsNil) 175 176 // Unlock the lock. 177 c.Assert(lock.Unlock(), IsNil) 178 179 // Run a flock command in another process, it should succeed because it can 180 // grab the lock again now. 181 cmd = exec.Command("flock", "--exclusive", "--nonblock", lock.Path(), "true") 182 c.Assert(cmd.Run(), IsNil) 183 } 184 185 // Test that locking a locked lock does nothing. 186 func (s *flockSuite) TestLockLocked(c *C) { 187 lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name")) 188 c.Assert(err, IsNil) 189 defer lock.Close() 190 191 // NOTE: technically this replaces the lock type but we only use LOCK_EX. 192 c.Assert(lock.Lock(), IsNil) 193 c.Assert(lock.Lock(), IsNil) 194 } 195 196 // Test that unlocking an unlocked lock does nothing. 197 func (s *flockSuite) TestUnlockUnlocked(c *C) { 198 lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name")) 199 c.Assert(err, IsNil) 200 defer lock.Close() 201 202 c.Assert(lock.Unlock(), IsNil) 203 } 204 205 // Test that locking or unlocking a closed lock fails. 206 func (s *flockSuite) TestUsingClosedLock(c *C) { 207 lock, err := osutil.NewFileLock(filepath.Join(c.MkDir(), "name")) 208 c.Assert(err, IsNil) 209 lock.Close() 210 211 c.Assert(lock.Lock(), ErrorMatches, "bad file descriptor") 212 c.Assert(lock.Unlock(), ErrorMatches, "bad file descriptor") 213 } 214 215 // Test that non-blocking locking reports error on pre-acquired lock. 216 func (s *flockSuite) TestLockUnlockNonblockingWorks(c *C) { 217 if os.Getenv("TRAVIS_BUILD_NUMBER") != "" { 218 c.Skip("Cannot use this under travis") 219 return 220 } 221 222 // Use the "flock" command to grab a lock for 9999 seconds in another process. 223 lockPath := filepath.Join(c.MkDir(), "lock") 224 sleeperKillerPath := filepath.Join(c.MkDir(), "pid") 225 // we can't use --no-fork because we still support 14.04 226 cmd := exec.Command("flock", "--exclusive", lockPath, "-c", fmt.Sprintf(`echo "kill $$" > %s && exec sleep 30`, sleeperKillerPath)) 227 c.Assert(cmd.Start(), IsNil) 228 defer func() { exec.Command("/bin/sh", sleeperKillerPath).Run() }() 229 230 // Give flock some chance to create the lock file. 231 for i := 0; i < 10; i++ { 232 if osutil.FileExists(lockPath) { 233 break 234 } 235 time.Sleep(time.Millisecond * 300) 236 } 237 238 // Try to acquire the same lock file and see that it is busy. 239 lock, err := osutil.NewFileLock(lockPath) 240 c.Assert(err, IsNil) 241 c.Assert(lock, NotNil) 242 defer lock.Close() 243 244 c.Assert(lock.TryLock(), Equals, osutil.ErrAlreadyLocked) 245 }