mirror of
https://github.com/hanzoai/go-fuse.git
synced 2026-10-30 14:46:18 +00:00
902 lines
21 KiB
Go
902 lines
21 KiB
Go
// Copyright 2019 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.
|
|
|
|
package fs
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/go-fuse/v2/fuse"
|
|
"github.com/hanzoai/go-fuse/v2/internal/testutil"
|
|
"github.com/hanzoai/go-fuse/v2/posixtest"
|
|
"github.com/kylelemons/godebug/pretty"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type testCase struct {
|
|
*testing.T
|
|
|
|
dir string
|
|
origDir string
|
|
mntDir string
|
|
|
|
loopback InodeEmbedder
|
|
rawFS fuse.RawFileSystem
|
|
server *fuse.Server
|
|
}
|
|
|
|
// writeOrig writes a file into the backing directory of the loopback mount
|
|
func (tc *testCase) writeOrig(path, content string, mode os.FileMode) {
|
|
if err := os.WriteFile(filepath.Join(tc.origDir, path), []byte(content), mode); err != nil {
|
|
tc.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (tc *testCase) clean() {
|
|
if err := tc.server.Unmount(); err != nil {
|
|
tc.Fatal(err)
|
|
}
|
|
}
|
|
|
|
type testOptions struct {
|
|
entryCache bool
|
|
enableLocks bool
|
|
attrCache bool
|
|
suppressDebug bool
|
|
testDir string
|
|
ro bool
|
|
directMount bool // sets MountOptions.DirectMount
|
|
directMountStrict bool // sets MountOptions.DirectMountStrict
|
|
disableSplice bool // sets MountOptions.DisableSplice
|
|
idMappedMount bool // sets MountOptions.IDMappedMount
|
|
}
|
|
|
|
// newTestCase creates the directories `orig` and `mnt` inside a temporary
|
|
// directory and mounts a loopback filesystem, backed by `orig`, on `mnt`.
|
|
func newTestCase(t *testing.T, opts *testOptions) *testCase {
|
|
if opts == nil {
|
|
opts = &testOptions{}
|
|
}
|
|
if opts.testDir == "" {
|
|
opts.testDir = t.TempDir()
|
|
}
|
|
tc := &testCase{
|
|
dir: opts.testDir,
|
|
T: t,
|
|
}
|
|
tc.origDir = tc.dir + "/orig"
|
|
tc.mntDir = tc.dir + "/mnt"
|
|
if err := os.Mkdir(tc.origDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(tc.mntDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var err error
|
|
tc.loopback, err = NewLoopbackRoot(tc.origDir)
|
|
if err != nil {
|
|
t.Fatalf("NewLoopback: %v", err)
|
|
}
|
|
|
|
oneSec := time.Second
|
|
|
|
attrDT := &oneSec
|
|
if !opts.attrCache {
|
|
attrDT = nil
|
|
}
|
|
entryDT := &oneSec
|
|
if !opts.entryCache {
|
|
entryDT = nil
|
|
}
|
|
tc.rawFS = NewNodeFS(tc.loopback, &Options{
|
|
EntryTimeout: entryDT,
|
|
AttrTimeout: attrDT,
|
|
Logger: log.New(os.Stderr, "", 0),
|
|
})
|
|
|
|
mOpts := &fuse.MountOptions{
|
|
DirectMount: opts.directMount,
|
|
DirectMountStrict: opts.directMountStrict,
|
|
EnableLocks: opts.enableLocks,
|
|
DisableSplice: opts.disableSplice,
|
|
IDMappedMount: opts.idMappedMount,
|
|
}
|
|
if !opts.suppressDebug {
|
|
mOpts.Debug = testutil.VerboseTest()
|
|
}
|
|
if opts.ro {
|
|
mOpts.Options = append(mOpts.Options, "ro")
|
|
}
|
|
if opts.idMappedMount {
|
|
mOpts.Options = append(mOpts.Options, "default_permissions")
|
|
}
|
|
tc.server, err = fuse.NewServer(tc.rawFS, tc.mntDir, mOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
go tc.server.Serve()
|
|
if err := tc.server.WaitMount(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(tc.clean)
|
|
|
|
return tc
|
|
}
|
|
|
|
func TestBasic(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
|
|
|
|
tc.writeOrig("file", "hello", 0644)
|
|
|
|
fn := tc.mntDir + "/file"
|
|
fi, err := os.Lstat(fn)
|
|
if err != nil {
|
|
t.Fatalf("Lstat: %v", err)
|
|
}
|
|
|
|
if fi.Size() != 5 {
|
|
t.Errorf("got size %d want 5", fi.Size())
|
|
}
|
|
|
|
stat := fuse.ToStatT(fi)
|
|
if got, want := uint32(stat.Mode), uint32(fuse.S_IFREG|0644); got != want {
|
|
t.Errorf("got mode %o, want %o", got, want)
|
|
}
|
|
|
|
if err := os.Remove(fn); err != nil {
|
|
t.Errorf("Remove: %v", err)
|
|
}
|
|
|
|
if fi, err := os.Lstat(fn); err == nil {
|
|
t.Errorf("Lstat after remove: got file %v", fi)
|
|
}
|
|
}
|
|
|
|
func TestExec(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
|
|
tc.writeOrig("test.sh", "#!/bin/sh\ntrue\n", 0755)
|
|
fn := tc.mntDir + "/test.sh"
|
|
if err := exec.Command("bash", "-c", fn).Run(); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFileFdLeak(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{
|
|
suppressDebug: false,
|
|
attrCache: true,
|
|
entryCache: true,
|
|
})
|
|
|
|
posixtest.FdLeak(t, tc.mntDir)
|
|
|
|
tc.clean()
|
|
bridge := tc.rawFS.(*rawBridge)
|
|
tc = nil
|
|
|
|
// posixtest.FdLeak also uses 15 as a limit.
|
|
if got, want := len(bridge.files), 15; got > want {
|
|
t.Errorf("found %d used file handles, should be <= %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNotifyEntry(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
|
|
|
|
orig := tc.origDir + "/file"
|
|
fn := tc.mntDir + "/file"
|
|
tc.writeOrig("file", "hello", 0644)
|
|
|
|
st := syscall.Stat_t{}
|
|
if err := syscall.Lstat(fn, &st); err != nil {
|
|
t.Fatalf("Lstat before: %v", err)
|
|
}
|
|
|
|
if err := os.Remove(orig); err != nil {
|
|
t.Fatalf("Remove: %v", err)
|
|
}
|
|
|
|
after := syscall.Stat_t{}
|
|
if err := syscall.Lstat(fn, &after); err != nil {
|
|
t.Fatalf("Lstat after: %v", err)
|
|
} else if !reflect.DeepEqual(st, after) {
|
|
t.Fatalf("got after %#v, want %#v", after, st)
|
|
}
|
|
|
|
if errno := tc.loopback.EmbeddedInode().NotifyEntry("file"); errno != 0 {
|
|
t.Errorf("notify failed: %v", errno)
|
|
}
|
|
|
|
if err := syscall.Lstat(fn, &after); err != syscall.ENOENT {
|
|
t.Fatalf("Lstat after: got %v, want ENOENT", err)
|
|
}
|
|
}
|
|
|
|
func TestReadDirStress(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
|
|
|
|
// Create 110 entries
|
|
for i := 0; i < 110; i++ {
|
|
name := fmt.Sprintf("file%036x", i)
|
|
if err := os.WriteFile(filepath.Join(tc.mntDir, name), []byte("hello"), 0644); err != nil {
|
|
t.Fatalf("WriteFile %q: %v", name, err)
|
|
}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
stress := func(gr int) {
|
|
defer wg.Done()
|
|
for i := 1; i < 100; i++ {
|
|
f, err := os.Open(tc.mntDir)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
_, err = f.Readdirnames(-1)
|
|
f.Close()
|
|
if err != nil {
|
|
t.Errorf("goroutine %d iteration %d: %v", gr, i, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
n := 3
|
|
for i := 1; i <= n; i++ {
|
|
wg.Add(1)
|
|
go stress(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
}
|
|
|
|
func TestStatFs(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
|
|
var before, after, mnt syscall.Statfs_t
|
|
if err := syscall.Statfs(tc.origDir, &before); err != nil {
|
|
t.Fatal("statfs before", err)
|
|
}
|
|
|
|
if err := syscall.Statfs(tc.mntDir, &mnt); err != nil {
|
|
t.Fatal("statfs mnt", err)
|
|
}
|
|
|
|
if err := syscall.Statfs(tc.origDir, &after); err != nil {
|
|
t.Fatal("statfs before", err)
|
|
}
|
|
|
|
var beforeFuse, afterFuse, mntFuse fuse.StatfsOut
|
|
mntFuse.FromStatfsT(&mnt)
|
|
beforeFuse.FromStatfsT(&before)
|
|
afterFuse.FromStatfsT(&after)
|
|
|
|
if !reflect.DeepEqual(beforeFuse, afterFuse) {
|
|
// This test is racy. If an external process consumes
|
|
// space while this runs, we may see spurious
|
|
// differences between the two statfs() calls.
|
|
t.Skipf("FS changed during test run, %v != %v", beforeFuse, afterFuse)
|
|
}
|
|
|
|
if !reflect.DeepEqual(mntFuse, beforeFuse) {
|
|
t.Errorf("Got %#v, want %#v", mntFuse, beforeFuse)
|
|
}
|
|
}
|
|
|
|
func TestGetAttrParallel(t *testing.T) {
|
|
// We grab a file-handle to provide to the API so rename+fstat
|
|
// can be handled correctly. Here, test that closing and
|
|
// (f)stat in parallel don't lead to fstat on closed files.
|
|
// We can only test that if we switch off caching
|
|
tc := newTestCase(t, &testOptions{suppressDebug: true})
|
|
|
|
N := 100
|
|
|
|
var fds []int
|
|
var fns []string
|
|
for i := 0; i < N; i++ {
|
|
fn := fmt.Sprintf("file%d", i)
|
|
tc.writeOrig(fn, "ello", 0644)
|
|
fn = filepath.Join(tc.mntDir, fn)
|
|
fns = append(fns, fn)
|
|
fd, err := syscall.Open(fn, syscall.O_RDONLY, 0)
|
|
if err != nil {
|
|
t.Fatalf("Open %d: %v", i, err)
|
|
}
|
|
|
|
fds = append(fds, fd)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2 * N)
|
|
for i := 0; i < N; i++ {
|
|
go func(i int) {
|
|
if err := syscall.Close(fds[i]); err != nil {
|
|
t.Errorf("close %d: %v", i, err)
|
|
}
|
|
wg.Done()
|
|
}(i)
|
|
go func(i int) {
|
|
var st syscall.Stat_t
|
|
if err := syscall.Lstat(fns[i], &st); err != nil {
|
|
t.Errorf("lstat %d: %v", i, err)
|
|
}
|
|
wg.Done()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestMknod(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{})
|
|
|
|
modes := map[string]uint32{
|
|
"regular": syscall.S_IFREG,
|
|
"socket": syscall.S_IFSOCK,
|
|
"fifo": syscall.S_IFIFO,
|
|
}
|
|
|
|
for nm, mode := range modes {
|
|
t.Run(nm, func(t *testing.T) {
|
|
p := filepath.Join(tc.mntDir, nm)
|
|
err := syscall.Mknod(p, mode|0755, (8<<8)|0)
|
|
if err != nil {
|
|
t.Fatalf("mknod(%s): %v", nm, err)
|
|
}
|
|
|
|
var st syscall.Stat_t
|
|
if err := syscall.Stat(p, &st); err != nil {
|
|
got := st.Mode &^ 07777
|
|
if want := uint(mode); want != uint(got) {
|
|
t.Fatalf("stat(%s): got %o want %o", nm, got, want)
|
|
}
|
|
}
|
|
|
|
// We could test if the files can be
|
|
// read/written but: The kernel handles FIFOs
|
|
// without talking to FUSE at all. Presumably,
|
|
// this also holds for sockets. Regular files
|
|
// are tested extensively elsewhere.
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMknodNotSupported(t *testing.T) {
|
|
mountPoint := t.TempDir()
|
|
|
|
server, err := Mount(mountPoint, &Inode{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("cannot mount: %v", err)
|
|
}
|
|
|
|
defer server.Unmount()
|
|
|
|
name := filepath.Join(mountPoint, "foo")
|
|
|
|
if got, want := syscall.Mknod(name, syscall.S_IFREG|0755, (8<<8)|0), syscall.ENOTSUP; got != want {
|
|
t.Fatalf("mknod: got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestPosix(t *testing.T) {
|
|
noisy := map[string]bool{
|
|
"ParallelFileOpen": true,
|
|
"ReadDir": true,
|
|
}
|
|
|
|
for nm, fn := range posixtest.All {
|
|
t.Run(nm, func(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{
|
|
suppressDebug: noisy[nm],
|
|
attrCache: true,
|
|
entryCache: true,
|
|
enableLocks: true,
|
|
})
|
|
|
|
fn(t, tc.mntDir)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReadDisableSplice(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{
|
|
disableSplice: true,
|
|
})
|
|
|
|
posixtest.FileBasic(t, tc.mntDir)
|
|
}
|
|
|
|
func TestOpenDirectIO(t *testing.T) {
|
|
// Apparently, tmpfs does not allow O_DIRECT, so try to create
|
|
// a test temp directory in /var/tmp.
|
|
ext4Dir, err := os.MkdirTemp("/var/tmp", "go-fuse.TestOpenDirectIO")
|
|
if err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
t.Cleanup(func() { os.RemoveAll(ext4Dir) })
|
|
|
|
posixtest.DirectIO(t, ext4Dir)
|
|
if t.Failed() {
|
|
t.Skip("DirectIO failed on underlying FS")
|
|
}
|
|
|
|
opts := testOptions{
|
|
testDir: ext4Dir,
|
|
attrCache: true,
|
|
entryCache: true,
|
|
}
|
|
|
|
tc := newTestCase(t, &opts)
|
|
posixtest.DirectIO(t, tc.mntDir)
|
|
}
|
|
|
|
// TestFsstress is loosely modeled after xfstest's fsstress. It performs rapid
|
|
// parallel removes / creates / readdirs. Coupled with inode reuse, this test
|
|
// used to deadlock go-fuse quite quickly.
|
|
//
|
|
// Note: Run as
|
|
//
|
|
// TMPDIR=/var/tmp go test -run TestFsstress
|
|
//
|
|
// to make sure the backing filesystem is ext4. /tmp is tmpfs on modern Linux
|
|
// distributions, and tmpfs does not reuse inode numbers, hiding the problem.
|
|
func TestFsstress(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
|
|
|
|
{
|
|
old := runtime.GOMAXPROCS(100)
|
|
defer runtime.GOMAXPROCS(old)
|
|
}
|
|
|
|
const concurrency = 10
|
|
var wg sync.WaitGroup
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// operations taking 1 path argument
|
|
ops1 := map[string]func(string) error{
|
|
"mkdir": func(p string) error { return syscall.Mkdir(p, 0700) },
|
|
"rmdir": func(p string) error { return syscall.Rmdir(p) },
|
|
"mknod_reg": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFREG, 0) },
|
|
"remove": os.Remove,
|
|
"unlink": syscall.Unlink,
|
|
"mknod_sock": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFSOCK, 0) },
|
|
"mknod_fifo": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFIFO, 0) },
|
|
"mkfifo": func(p string) error { return syscall.Mkfifo(p, 0700) },
|
|
"symlink": func(p string) error { return syscall.Symlink("foo", p) },
|
|
"creat": func(p string) error {
|
|
fd, err := syscall.Open(p, syscall.O_CREAT|syscall.O_EXCL, 0700)
|
|
if err == nil {
|
|
syscall.Close(fd)
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
// operations taking 2 path arguments
|
|
ops2 := map[string]func(string, string) error{
|
|
"rename": syscall.Rename,
|
|
"link": syscall.Link,
|
|
}
|
|
|
|
type opStats struct {
|
|
ok *int64
|
|
fail *int64
|
|
hung *int64
|
|
}
|
|
stats := make(map[string]opStats)
|
|
|
|
// pathN() returns something like /var/tmp/TestFsstress/TestFsstress.4
|
|
pathN := func(n int) string {
|
|
return fmt.Sprintf("%s/%s.%d", tc.mntDir, t.Name(), n)
|
|
}
|
|
|
|
opLoop := func(k string, n int) {
|
|
defer wg.Done()
|
|
op := ops1[k]
|
|
for {
|
|
p := pathN(1)
|
|
atomic.AddInt64(stats[k].hung, 1)
|
|
err := op(p)
|
|
atomic.AddInt64(stats[k].hung, -1)
|
|
if err != nil {
|
|
atomic.AddInt64(stats[k].fail, 1)
|
|
} else {
|
|
atomic.AddInt64(stats[k].ok, 1)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
op2Loop := func(k string, n int) {
|
|
defer wg.Done()
|
|
op := ops2[k]
|
|
n2 := (n + 1) % concurrency
|
|
for {
|
|
p1 := pathN(n)
|
|
p2 := pathN(n2)
|
|
atomic.AddInt64(stats[k].hung, 1)
|
|
err := op(p1, p2)
|
|
atomic.AddInt64(stats[k].hung, -1)
|
|
if err != nil {
|
|
atomic.AddInt64(stats[k].fail, 1)
|
|
} else {
|
|
atomic.AddInt64(stats[k].ok, 1)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
readdirLoop := func(k string) {
|
|
defer wg.Done()
|
|
for {
|
|
atomic.AddInt64(stats[k].hung, 1)
|
|
f, err := os.Open(tc.mntDir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
_, err = f.Readdir(0)
|
|
if err != nil {
|
|
atomic.AddInt64(stats[k].fail, 1)
|
|
} else {
|
|
atomic.AddInt64(stats[k].ok, 1)
|
|
}
|
|
f.Close()
|
|
atomic.AddInt64(stats[k].hung, -1)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// prepare stats map
|
|
var allOps []string
|
|
for k := range ops1 {
|
|
allOps = append(allOps, k)
|
|
}
|
|
for k := range ops2 {
|
|
allOps = append(allOps, k)
|
|
}
|
|
allOps = append(allOps, "readdir")
|
|
for _, k := range allOps {
|
|
var i1, i2, i3 int64
|
|
stats[k] = opStats{ok: &i1, fail: &i2, hung: &i3}
|
|
}
|
|
|
|
// spawn worker goroutines
|
|
for i := 0; i < concurrency; i++ {
|
|
for k := range ops1 {
|
|
wg.Add(1)
|
|
go opLoop(k, i)
|
|
}
|
|
for k := range ops2 {
|
|
wg.Add(1)
|
|
go op2Loop(k, i)
|
|
}
|
|
}
|
|
{
|
|
k := "readdir"
|
|
wg.Add(1)
|
|
go readdirLoop(k)
|
|
}
|
|
|
|
// spawn ls loop
|
|
//
|
|
// An external "ls" loop has a destructive effect that I am unable to
|
|
// reproduce through in-process operations.
|
|
if strings.ContainsAny(tc.mntDir, "'\\") {
|
|
// But let's not enable shell injection.
|
|
log.Panicf("shell injection attempt? mntDir=%q", tc.mntDir)
|
|
}
|
|
// --color=always enables xattr lookups for extra stress
|
|
cmd := exec.Command("bash", "-c", "while true ; do ls -l --color=always '"+tc.mntDir+"'; done")
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wg.Add(1)
|
|
go func() {
|
|
cmd.Wait()
|
|
wg.Done()
|
|
}()
|
|
|
|
defer cmd.Process.Kill()
|
|
|
|
// Run the test for 1 second. If it deadlocks, it usually does within 20ms.
|
|
time.Sleep(1 * time.Second)
|
|
|
|
cancel()
|
|
cmd.Process.Kill()
|
|
|
|
// waitTimeout waits for the waitgroup for the specified max timeout.
|
|
// Returns true if waiting timed out.
|
|
waitTimeout := func(wg *sync.WaitGroup, timeout time.Duration) bool {
|
|
c := make(chan struct{})
|
|
go func() {
|
|
defer close(c)
|
|
wg.Wait()
|
|
}()
|
|
select {
|
|
case <-c:
|
|
return false // completed normally
|
|
case <-time.After(timeout):
|
|
return true // timed out
|
|
}
|
|
}
|
|
|
|
if waitTimeout(&wg, time.Second) {
|
|
t.Errorf("timeout waiting for goroutines to exit (deadlocked?)")
|
|
}
|
|
|
|
// Print operation statistics
|
|
var keys []string
|
|
for k := range stats {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
t.Logf("Operation statistics:")
|
|
for _, k := range keys {
|
|
v := stats[k]
|
|
t.Logf("%10s: %5d ok, %6d fail, %2d hung", k, *v.ok, *v.fail, *v.hung)
|
|
}
|
|
}
|
|
|
|
// TestStaleHardlinks creates a lot of hard links and deletes them again
|
|
// behind the back of the loopback fs. Then opens the original file.
|
|
//
|
|
// Fails at the moment. Core of the problem:
|
|
//
|
|
// 18:41:50.796468 rx 136: LOOKUP n1 ["link0"] 6b
|
|
// 18:41:50.796489 tx 136: OK, {n2 g1 tE=0s tA=0s {M0100600 SZ=0 L=1 1026:1026 B0*4096 i0:269663 A 1616348510.793212 M 1616348510.793212 C 1616348510.795212}}
|
|
// 18:41:50.796535 rx 138: OPEN n2 {O_RDONLY,0x8000}
|
|
// 18:41:50.796557 tx 138: 2=no such file or directory, {Fh 0 }
|
|
func TestStaleHardlinks(t *testing.T) {
|
|
// Disable all caches we can disable
|
|
tc := newTestCase(t, &testOptions{attrCache: false, entryCache: false})
|
|
|
|
// gvfsd-trash sets an inotify watch on mntDir and stat()s every file that is
|
|
// created, racing with the test logic ( https://github.com/hanwen/go-fuse/issues/478 ).
|
|
// Use a subdir to prevent that.
|
|
if err := os.Mkdir(tc.mntDir+"/x", 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// "link0" is original file
|
|
link0 := tc.mntDir + "/x/link0"
|
|
if fd, err := unix.Open(link0, unix.O_CREAT, 0600); err != nil {
|
|
t.Fatal(err)
|
|
} else {
|
|
syscall.Close(fd)
|
|
}
|
|
// Create hardlinks via mntDir
|
|
t.Logf("create link1...20, pid=%d", os.Getpid())
|
|
for i := 1; i < 20; i++ {
|
|
linki := fmt.Sprintf(tc.mntDir+"/x/link%d", i)
|
|
if err := syscall.Link(link0, linki); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Delete hardlinks via origDir (behind loopback fs's back)
|
|
t.Log("delete link1...20 behind loopback's back")
|
|
for i := 1; i < 20; i++ {
|
|
linki := fmt.Sprintf(tc.origDir+"/x/link%d", i)
|
|
if err := syscall.Unlink(linki); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Try to open link0 via mntDir
|
|
t.Log("open link0")
|
|
fd, err := syscall.Open(link0, syscall.O_RDONLY, 0)
|
|
if err != nil {
|
|
t.Error(err)
|
|
} else {
|
|
syscall.Close(fd)
|
|
}
|
|
|
|
}
|
|
|
|
func init() {
|
|
syscall.Umask(0)
|
|
}
|
|
|
|
func testMountDir(dir string) error {
|
|
opts := &Options{}
|
|
opts.Debug = testutil.VerboseTest()
|
|
server, err := Mount(dir, &Inode{}, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
server.Unmount()
|
|
server.Wait()
|
|
return nil
|
|
}
|
|
|
|
func TestParallelMount(t *testing.T) {
|
|
before := runtime.GOMAXPROCS(1)
|
|
defer runtime.GOMAXPROCS(before)
|
|
// Per default, only 1000 FUSE mounts are allowed, then you get
|
|
// > /usr/bin/fusermount3: too many FUSE filesystems mounted; mount_max=N can be set in /etc/fuse.conf
|
|
// Let's stay well below 1000.
|
|
N := 100
|
|
todo := make(chan string, N)
|
|
result := make(chan error, N)
|
|
for i := 0; i < N; i++ {
|
|
todo <- t.TempDir()
|
|
}
|
|
close(todo)
|
|
|
|
P := 2
|
|
for i := 0; i < P; i++ {
|
|
go func() {
|
|
for d := range todo {
|
|
result <- testMountDir(d)
|
|
}
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < N; i++ {
|
|
e := <-result
|
|
if e != nil {
|
|
t.Error(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
type handleLessCreateNode struct {
|
|
Inode
|
|
}
|
|
|
|
var _ = (NodeCreater)((*handleLessCreateNode)(nil))
|
|
|
|
func (n *handleLessCreateNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *Inode, fh FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
|
f := &MemRegularFile{
|
|
Attr: fuse.Attr{
|
|
Mode: mode,
|
|
},
|
|
}
|
|
ch := n.NewPersistentInode(ctx, f, StableAttr{Mode: fuse.S_IFREG})
|
|
n.AddChild(name, ch, true)
|
|
return ch, nil, fuse.FOPEN_KEEP_CACHE, 0
|
|
}
|
|
|
|
func TestHandleLessCreate(t *testing.T) {
|
|
hlcn := &handleLessCreateNode{}
|
|
dir, _ := testMount(t, hlcn, nil)
|
|
|
|
posixtest.FileBasic(t, dir)
|
|
}
|
|
|
|
func lstatxPath(p string) (*unix.Statx_t, error) {
|
|
var r unix.Statx_t
|
|
err := unix.Statx(unix.AT_FDCWD, p, unix.AT_SYMLINK_NOFOLLOW,
|
|
(unix.STATX_BASIC_STATS | unix.STATX_BTIME |
|
|
unix.STATX_MNT_ID), // unix.STATX_DIOALIGN
|
|
&r)
|
|
return &r, err
|
|
}
|
|
|
|
func clearStatx(st *unix.Statx_t, mask uint32) {
|
|
st.Mask = mask
|
|
if mask&(unix.STATX_TYPE|unix.STATX_MODE) == 0 {
|
|
st.Mode = 0
|
|
}
|
|
if mask&(unix.STATX_NLINK) == 0 {
|
|
st.Nlink = 0
|
|
}
|
|
if mask&(unix.STATX_INO) == 0 {
|
|
st.Ino = 0
|
|
}
|
|
if mask&(unix.STATX_ATIME) == 0 {
|
|
st.Atime = unix.StatxTimestamp{}
|
|
}
|
|
if mask&(unix.STATX_BTIME) == 0 {
|
|
st.Btime = unix.StatxTimestamp{}
|
|
}
|
|
if mask&(unix.STATX_CTIME) == 0 {
|
|
st.Ctime = unix.StatxTimestamp{}
|
|
}
|
|
if mask&(unix.STATX_MTIME) == 0 {
|
|
st.Mtime = unix.StatxTimestamp{}
|
|
}
|
|
|
|
st.Dev_minor = 0
|
|
st.Dev_major = 0
|
|
st.Mnt_id = 0
|
|
st.Attributes_mask = 0
|
|
}
|
|
|
|
func TestStatx(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{attrCache: false, entryCache: false})
|
|
if err := os.WriteFile(tc.origDir+"/file", []byte("blabla"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oFile := tc.origDir + "/file"
|
|
want, err := lstatxPath(oFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mFile := tc.mntDir + "/file"
|
|
got, err := lstatxPath(mFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mask := got.Mask & want.Mask
|
|
clearStatx(got, mask)
|
|
clearStatx(want, mask)
|
|
if diff := pretty.Compare(got, want); diff != "" {
|
|
t.Errorf("got, want: %s", diff)
|
|
}
|
|
|
|
// the following works, but does not set Fh in StatxIn
|
|
oFD, err := os.Open(oFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer oFD.Close()
|
|
mFD, err := os.Open(mFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer mFD.Close()
|
|
|
|
var osx, msx unix.Statx_t
|
|
if err := unix.Statx(int(oFD.Fd()), "", unix.AT_EMPTY_PATH,
|
|
(unix.STATX_BASIC_STATS | unix.STATX_BTIME |
|
|
unix.STATX_MNT_ID), // unix.STATX_DIOALIGN
|
|
&osx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Statx(int(mFD.Fd()), "", unix.AT_EMPTY_PATH,
|
|
(unix.STATX_BASIC_STATS | unix.STATX_BTIME |
|
|
unix.STATX_MNT_ID), // unix.STATX_DIOALIGN
|
|
&msx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mask = msx.Mask & osx.Mask
|
|
clearStatx(&msx, mask)
|
|
clearStatx(&osx, mask)
|
|
if diff := pretty.Compare(msx, osx); diff != "" {
|
|
t.Errorf("got, want: %s", diff)
|
|
}
|
|
}
|
|
|
|
// A dangling symlink should not cause lchown to fail with ENOENT.
|
|
// This is used in apt-get install.
|
|
func TestLChown(t *testing.T) {
|
|
tc := newTestCase(t, &testOptions{})
|
|
if err := os.Symlink("doesnotexist", tc.origDir+"/link"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.Lchown(tc.mntDir+"/link", os.Getuid(), os.Getgid()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|