Files
go-fuse/fuse/test/file_lock_test.go

237 lines
5.5 KiB
Go

// Copyright 2018 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
// +build linux
package test
import (
"os"
"os/exec"
"path/filepath"
"sync"
"syscall"
"testing"
"github.com/hanzoai/go-fuse/v2/fuse"
"github.com/hanzoai/go-fuse/v2/fuse/nodefs"
"github.com/hanzoai/go-fuse/v2/internal/testutil"
)
func TestFlockExclusive(t *testing.T) {
cmd, err := exec.LookPath("flock")
if err != nil {
t.Skip("flock command not found.")
}
tc := NewTestCase(t)
defer tc.Cleanup()
contents := []byte{1, 2, 3}
tc.WriteFile(tc.origFile, []byte(contents), 0700)
for {
f, err := os.Open("/dev/null")
if err != nil {
t.Fatalf("Open(/dev/null): %v", err)
}
defer f.Close()
if f.Fd() > 3 {
break
}
}
f, err := os.OpenFile(tc.mountFile, os.O_WRONLY, 0)
if err != nil {
t.Fatalf("OpenFile(%q): %v", tc.mountFile, err)
}
defer f.Close()
if err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
t.Errorf("Flock returned: %v", err)
return
}
_, err = runExternalFlock(cmd, tc.mountFile)
if err == nil {
t.Errorf("Expected flock to fail, but it did not")
}
}
func runExternalFlock(flockPath, fname string) ([]byte, error) {
f, err := os.OpenFile(fname, os.O_WRONLY, 0)
if err != nil {
return nil, err
}
defer f.Close()
// in order to test the lock property we must use cmd.ExtraFiles (instead of passing the actual file)
// if we were to pass the file then this flock command would fail to place the lock (returning a
// 'file busy' error) as it is already opened and locked at this point (see above)
cmd := exec.Command(flockPath, "--exclusive", "--nonblock", "3")
cmd.Env = append(cmd.Env, "LC_ALL=C") // in case the user's shell language is different
cmd.ExtraFiles = []*os.File{f}
return cmd.CombinedOutput()
}
type lockingNode struct {
nodefs.Node
mu sync.Mutex
getLkInvoked bool
setLkInvoked bool
setLkwInvoked bool
}
func (n *lockingNode) GetLkInvoked() bool {
n.mu.Lock()
defer n.mu.Unlock()
return n.getLkInvoked
}
func (n *lockingNode) SetLkInvoked() bool {
n.mu.Lock()
defer n.mu.Unlock()
return n.setLkInvoked
}
func (n *lockingNode) SetLkwInvoked() bool {
n.mu.Lock()
defer n.mu.Unlock()
return n.setLkwInvoked
}
func (n *lockingNode) Open(flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) {
return nodefs.NewDataFile([]byte("hello world")), fuse.OK
}
func (n *lockingNode) GetLk(file nodefs.File, owner uint64, lk *fuse.FileLock, flags uint32, out *fuse.FileLock, context *fuse.Context) (code fuse.Status) {
n.mu.Lock()
defer n.mu.Unlock()
n.getLkInvoked = true
return fuse.OK
}
func (n *lockingNode) SetLk(file nodefs.File, owner uint64, lk *fuse.FileLock, flags uint32, context *fuse.Context) (code fuse.Status) {
n.mu.Lock()
defer n.mu.Unlock()
n.setLkInvoked = true
return fuse.OK
}
func (n *lockingNode) SetLkw(file nodefs.File, owner uint64, lk *fuse.FileLock, flags uint32, context *fuse.Context) (code fuse.Status) {
n.mu.Lock()
defer n.mu.Unlock()
n.setLkwInvoked = true
return fuse.OK
}
func TestFlockInvoked(t *testing.T) {
flock, err := exec.LookPath("flock")
if err != nil {
t.Skip("flock command not found.")
}
dir := t.TempDir()
opts := &nodefs.Options{
Owner: fuse.CurrentOwner(),
Debug: testutil.VerboseTest(),
}
root := nodefs.NewDefaultNode()
conn := nodefs.NewFileSystemConnector(root, opts)
mountOpts := fuse.MountOptions{
EnableLocks: true,
}
s, err := fuse.NewServer(conn.RawFS(), dir, &mountOpts)
if err != nil {
t.Fatal("NewServer", err)
}
go s.Serve()
if err := s.WaitMount(); err != nil {
t.Fatal("WaitMount", err)
}
defer s.Unmount()
node := &lockingNode{
Node: nodefs.NewDefaultNode(),
}
root.Inode().NewChild("foo", false, node)
realPath := filepath.Join(dir, "foo")
if node.SetLkInvoked() {
t.Fatalf("SetLk is invoked")
}
cmd := exec.Command(flock, "--nonblock", realPath, "echo", "locked")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("flock %v: %v", err, string(out))
}
if !node.SetLkInvoked() {
t.Fatalf("SetLk is not invoked")
}
if node.SetLkwInvoked() {
t.Fatalf("SetLkw is invoked")
}
cmd = exec.Command(flock, realPath, "echo", "locked")
out, err = cmd.CombinedOutput()
if err != nil {
t.Fatalf("flock %v: %v", err, string(out))
}
if !node.SetLkwInvoked() {
t.Fatalf("SetLkw is not invoked")
}
}
// Test that file system that don't implement locking are still
// handled in the VFS layer.
func TestNoLockSupport(t *testing.T) {
flock, err := exec.LookPath("flock")
if err != nil {
t.Skip("flock command not found.")
}
tmp := t.TempDir()
mnt := t.TempDir()
opts := &nodefs.Options{
Owner: fuse.CurrentOwner(),
Debug: testutil.VerboseTest(),
}
root := nodefs.NewMemNodeFSRoot(tmp)
lock := fuse.FileLock{}
outLock := fuse.FileLock{}
ctx := fuse.Context{}
if status := root.GetLk(nil, uint64(1), &lock, uint32(0x0), &outLock, &ctx); status != fuse.ENOSYS {
t.Fatalf("MemNodeFs should not implement locking")
}
s, _, err := nodefs.MountRoot(mnt, root, opts)
if err != nil {
t.Fatalf("MountRoot: %v", err)
}
go s.Serve()
if err := s.WaitMount(); err != nil {
t.Fatalf("WaitMount: %v", err)
}
defer s.Unmount()
fn := mnt + "/file.txt"
if err := os.WriteFile(fn, []byte("content"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
cmd := exec.Command(flock, "--nonblock", fn, "echo", "locked")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("flock %v: %v", err, string(out))
}
}