Commit eb4d413d authored by Kirill Smelkov's avatar Kirill Smelkov

Merge branch 'master' into y/nodefs-cancel

Notable patches:

- 265a3926 "fuse: Increase MAX_KERNEL_WRITE to 1 MiB & enable CAP_MAX_PAGES"

  this should affect WCFS performance significantly because previously data was
  read from WCFS server in 128KB chunks and now it could be read in 2MB.

  For the reference: WCFS was setting MaxWrite=2MB from the beginning, and
  previously it was capped to 128KB by go-fuse. Now with go-fuse upgrade we
  should hopefully automatically get increase in performance.

- 90b055af "fusermount: Fix option escaping"

  Levin's patch to allow ',' to be present in fsname, so that we can mount
  neo:// with multiple masters.

  See nexedi/wendelin.core!15 for context
  and details.

* master: (45 commits)
  fs: kill subprocess before tearing down test
  fuse/test: Fix TestFopenKeepCache, take 2
  fs: simplify TestReadDirStress
  fuse: tweak Unmount doc comment
  .github: add Go 1.20
  fuse/mount_linux_test/DirectMount: Verify FsName \w comma/backslash works
  fusermount: Fix option escaping
  fuse: fix debug print for FsyncDir
  fuse: print GETATTR flags in input
  fs: document FileHandle argument for Getattr
  README.md: tweak & polish
  Drop "// " from LICENSE
  fs: fix NodeLookuper documentation
  .github: set user_allow_other in /etc/fuse.conf
  .github: drop Go 1.13 / 1.15, add Go 1.18/1.19
  fuse: MountDirect: always pass max_read
  tests: TestDirectMount: better coverage, clearer output
  fs: document Readdir determinism requirement
  fs: fold duplicate fuse.Context{} instantiations
  newunionfs: make readdir deterministic
  ...
parents 6e5a2cc2 255ab741
......@@ -11,10 +11,11 @@ jobs:
strategy:
matrix:
go:
- "1.13.x" # Ubuntu 20.04 LTS "focal"
- "1.15.x" # Debian 11 "Bullseye"
- "1.16.x" # Golang upstream stable
- "1.17.x" # Golang upstream stable
- "1.18.x" # Golang upstream stable
- "1.19.x" # Golang upstream stable
- "1.20.x" # Golang upstream stable
# Don't cancel everything when one Go version fails
fail-fast: false
runs-on: ubuntu-latest
......@@ -31,6 +32,7 @@ jobs:
# CI platform specific setup steps happen here
- run: sudo apt-get install -qq fuse3 libssl-dev libfuse-dev
- run: echo user_allow_other | sudo tee -a /etc/fuse.conf
# Actual test steps are in all.bash
- run: ./all.bash
// New BSD License
//
// Copyright (c) 2010 the Go-FUSE Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Ivan Krasin nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
New BSD License
Copyright (c) 2010 the Go-FUSE Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Ivan Krasin nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# GO-FUSE
# Go-FUSE
[![CI](https://github.com/hanwen/go-fuse/actions/workflows/ci.yml/badge.svg)](https://github.com/hanwen/go-fuse/actions/workflows/ci.yml)
[![GoDoc](https://godoc.org/github.com/hanwen/go-fuse?status.svg)](https://godoc.org/github.com/hanwen/go-fuse/v2)
[![GoDoc](https://godoc.org/github.com/hanwen/go-fuse/v2/fs?status.svg)](https://godoc.org/github.com/hanwen/go-fuse/v2/fs)
Go native bindings for the FUSE kernel module.
......@@ -20,7 +20,7 @@ and
The FUSE library gained a new, cleaned-up API during a rewrite
completed in 2019. Find extensive documentation
[here](https://godoc.org/github.com/hanwen/go-fuse/v2).
[here](https://godoc.org/github.com/hanwen/go-fuse/v2/fs).
Further highlights of this library is
......@@ -31,9 +31,9 @@ Further highlights of this library is
## Examples
* `example/hello/main.go` contains a 60-line "hello world" filesystem
* [example/hello/](example/hello/main.go) contains a 60-line "hello world" filesystem
* `zipfs/zipfs.go` contains a small and simple read-only filesystem for
* [zipfs/zipfs](zipfs/zipfs.go) contains a small and simple read-only filesystem for
zip and tar files. The corresponding command is in example/zipfs/
For example,
......@@ -44,10 +44,10 @@ Further highlights of this library is
fusermount -u /tmp/mountpoint
````
* `zipfs/multizipfs.go` shows how to use in-process mounts to
combine multiple Go-FUSE filesystems into a larger filesystem.
* [zipfs/multizipfs](zipfs/multizipfs.go) shows how to use combine
simple Go-FUSE filesystems into a larger filesystem.
* `fuse/loopback.go` mounts another piece of the filesystem.
* [example/loopback](example/loopback/main.go) mounts another piece of the filesystem.
Functionally, it is similar to a symlink. A binary to run is in
example/loopback/ . For example
......@@ -60,7 +60,7 @@ Further highlights of this library is
## macOS Support
go-fuse works somewhat on OSX. Known limitations:
Go-FUSE works somewhat on OSX. Known limitations:
* All of the limitations of OSXFUSE, including lack of support for
NOTIFY.
......@@ -82,18 +82,13 @@ go-fuse works somewhat on OSX. Known limitations:
## Bugs
Yes, probably. Report them through
https://github.com/hanwen/go-fuse/issues
https://github.com/hanwen/go-fuse/issues. Please include a debug trace
(set `fuse.MountOptions.Debug` to `true`).
## Disclaimer
This is not an official Google product.
## Known Problems
Grep source code for TODO. Major topics:
* Missing support for `CUSE`, `BMAP`, `IOCTL`
## License
Like Go, this library is distributed under the new BSD license. See
......
......@@ -13,7 +13,11 @@ GOOS=darwin go build ./fuse/... ./fs/... ./example/loopback/...
# -p 1 .......... Run tests serially, which also means we get live output
# instead of per-package buffering.
# -count 1 ...... Disable result caching, so we can see flakey tests
go test -timeout 5m -p 1 -count 1 ./...
GO_TEST="go test -timeout 5m -p 1 -count 1"
# Run all tests as current user
$GO_TEST ./...
# Direct-mount tests need to run as root
sudo env PATH=$PATH $GO_TEST -run TestDirectMount ./fs ./fuse
make -C benchmark
go test ./benchmark -test.bench '.*' -test.cpu 1,2
......@@ -25,7 +25,10 @@ import (
func setupFs(node fs.InodeEmbedder, N int) (string, func()) {
opts := &fs.Options{}
opts.Debug = testutil.VerboseTest()
mountPoint := testutil.TempDir()
mountPoint, err := os.MkdirTemp("", "")
if err != nil {
log.Panicf("TempDir: %v", err)
}
server, err := fs.Mount(mountPoint, node, opts)
if err != nil {
log.Panicf("cannot mount %v", err)
......@@ -53,8 +56,6 @@ func setupFs(node fs.InodeEmbedder, N int) (string, func()) {
err := server.Unmount()
if err != nil {
log.Println("error during unmount", err)
} else {
os.RemoveAll(mountPoint)
}
}
}
......@@ -251,8 +252,10 @@ func BenchmarkCFuseThreadedStat(b *testing.B) {
}
f.Close()
mountPoint := testutil.TempDir()
defer os.RemoveAll(mountPoint)
mountPoint, err := os.MkdirTemp("", "")
if err != nil {
b.Fatalf("MkdirTemp: %v", err)
}
cmd := exec.Command(wd+"/cstatfs",
"-o",
......
......@@ -19,6 +19,7 @@ import (
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
func writeMemProfile(fn string, sigs <-chan os.Signal) {
......@@ -47,6 +48,8 @@ func main() {
other := flag.Bool("allow-other", false, "mount with -o allowother.")
quiet := flag.Bool("q", false, "quiet")
ro := flag.Bool("ro", false, "mount read-only")
directmount := flag.Bool("directmount", false, "try to call the mount syscall instead of executing fusermount")
directmountstrict := flag.Bool("directmountstrict", false, "like directmount, but don't fall back to fusermount")
cpuprofile := flag.String("cpuprofile", "", "write cpu profile to this file")
memprofile := flag.String("memprofile", "", "write memory profile to this file")
flag.Parse()
......@@ -90,13 +93,22 @@ func main() {
sec := time.Second
opts := &fs.Options{
// These options are to be compatible with libfuse defaults,
// The timeout options are to be compatible with libfuse defaults,
// making benchmarking easier.
AttrTimeout: &sec,
EntryTimeout: &sec,
NullPermissions: true, // Leave file permissions on "000" files as-is
MountOptions: fuse.MountOptions{
AllowOther: *other,
Debug: *debug,
DirectMount: *directmount,
DirectMountStrict: *directmountstrict,
FsName: orig, // First column in "df -T": original dir
Name: "loopback", // Second column in "df -T" will be shown as "fuse." + Name
},
}
opts.Debug = *debug
opts.AllowOther = *other
if opts.AllowOther {
// Make the kernel check file permissions for us
opts.MountOptions.Options = append(opts.MountOptions.Options, "default_permissions")
......@@ -104,12 +116,6 @@ func main() {
if *ro {
opts.MountOptions.Options = append(opts.MountOptions.Options, "ro")
}
// First column in "df -T": original dir
opts.MountOptions.Options = append(opts.MountOptions.Options, "fsname="+orig)
// Second column in "df -T" will be shown as "fuse." + Name
opts.MountOptions.Name = "loopback"
// Leave file permissions on "000" files as-is
opts.NullPermissions = true
// Enable diagnostics logging
if !*quiet {
opts.Logger = log.New(os.Stderr, "", 0)
......@@ -121,5 +127,13 @@ func main() {
if !*quiet {
fmt.Println("Mounted!")
}
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
server.Unmount()
}()
server.Wait()
}
......@@ -4,7 +4,7 @@
// Package fs provides infrastructure to build tree-organized filesystems.
//
// Structure of a file system implementation
// # Structure of a file system implementation
//
// To create a file system, you should first define types for the
// nodes of the file system tree.
......@@ -19,9 +19,11 @@
// // Node types should implement some file system operations, eg. Lookup
// var _ = (fs.NodeLookuper)((*myNode)(nil))
//
// func (n *myNode) Lookup(ctx context.Context, name string, ... ) (*Inode, syscall.Errno) {
// func (n *myNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
// ops := myNode{}
// return n.NewInode(ctx, &ops, fs.StableAttr{Mode: syscall.S_IFDIR}), 0
// out.Mode = 0755
// out.Size = 42
// return n.NewInode(ctx, &ops, fs.StableAttr{Mode: syscall.S_IFREG}), 0
// }
//
// The method names are inspired on the system call names, so we have
......@@ -34,13 +36,13 @@
// // start serving the file system
// server.Wait()
//
// Error handling
// # Error handling
//
// All error reporting must use the syscall.Errno type. This is an
// integer with predefined error codes, where the value 0 (`OK`)
// should be used to indicate success.
//
// File system concepts
// # File system concepts
//
// The FUSE API is very similar to Linux' internal VFS API for
// defining file systems in the kernel. It is therefore useful to
......@@ -87,8 +89,7 @@
// Go-FUSE, but the result of Lookup operation essentially is a
// dirent, which the kernel puts in a cache.
//
//
// Kernel caching
// # Kernel caching
//
// The kernel caches several pieces of information from the FUSE process:
//
......@@ -123,13 +124,13 @@
// AttrTimeout: &sec,
// }
//
// Locking
// # Locking
//
// Locks for networked filesystems are supported through the suite of
// Getlk, Setlk and Setlkw methods. They alllow locks on regions of
// regular files.
//
// Parallelism
// # Parallelism
//
// The VFS layer in the kernel is optimized to be highly parallel, and
// this parallelism also affects FUSE file systems: many FUSE
......@@ -138,7 +139,7 @@
// system issuing file operations in parallel, and using the race
// detector to weed out data races.
//
// Dynamically discovered file systems
// # Dynamically discovered file systems
//
// File system data usually cannot fit all in RAM, so the kernel must
// discover the file system dynamically: as you are entering and list
......@@ -151,7 +152,7 @@
// individual children of directories, and 2. Readdir, part of the
// NodeReaddirer interface for listing the contents of a directory.
//
// Static in-memory file systems
// # Static in-memory file systems
//
// For small, read-only file systems, getting the locking mechanics of
// Lookup correct is tedious, so Go-FUSE provides a feature to
......@@ -224,7 +225,10 @@ type NodeAccesser interface {
// returning zeroed permissions, the default behavior is to change the
// mode of 0755 (directory) or 0644 (files). This can be switched off
// with the Options.NullPermissions setting. If blksize is unset, 4096
// is assumed, and the 'blocks' field is set accordingly.
// is assumed, and the 'blocks' field is set accordingly. The 'f'
// argument is provided for consistency, however, in practice the
// kernel never sends a file handle, even if the Getattr call
// originated from a fstat system call.
type NodeGetattrer interface {
Getattr(ctx context.Context, f FileHandle, out *fuse.AttrOut) syscall.Errno
}
......@@ -400,8 +404,6 @@ type DirStream interface {
// example, the Symlink, Create, Mknod, Link methods all create new
// children in directories. Hence, they also return *Inode and must
// populate their fuse.EntryOut arguments.
//
type NodeLookuper interface {
Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno)
}
......@@ -420,7 +422,10 @@ type NodeOpendirer interface {
// for Readdir to return different results from Lookup. For example,
// you can return nothing for Readdir ("ls my-fuse-mount" is empty),
// while still implementing Lookup ("ls my-fuse-mount/a-specific-file"
// shows a single file).
// shows a single file). The DirStream returned must be deterministic;
// a randomized result (e.g. due to map iteration) can lead to entries
// disappearing if multiple processes read the same directory
// concurrently.
//
// If a directory does not implement NodeReaddirer, a list of
// currently known children from the tree is returned. This means that
......@@ -610,4 +615,8 @@ type Options struct {
// return error, but want to signal something seems off
// anyway. If unset, no messages are printed.
Logger *log.Logger
// RootStableAttr is an optional way to set e.g. Ino and/or Gen for
// the root directory when calling fs.Mount(), Mode is ignored.
RootStableAttr *StableAttr
}
This diff is collapsed.
......@@ -22,7 +22,6 @@ import (
func TestBridgeReaddirPlusVirtualEntries(t *testing.T) {
// Set suppressDebug as we do our own logging
tc := newTestCase(t, &testOptions{suppressDebug: true})
defer tc.Clean()
rb := tc.rawFS.(*rawBridge)
......@@ -92,8 +91,7 @@ func TestBridgeReaddirPlusVirtualEntries(t *testing.T) {
// we just have not received the FORGET yet.
func TestTypeChange(t *testing.T) {
rootNode := testTypeChangeIno{}
mnt, _, clean := testMount(t, &rootNode, nil)
defer clean()
mnt, _ := testMount(t, &rootNode, nil)
for i := 0; i < 100; i++ {
fi, _ := os.Stat(mnt + "/file")
......@@ -141,8 +139,7 @@ func (fn *testTypeChangeIno) Lookup(ctx context.Context, name string, out *fuse.
// disconnected from the hierarchy (=orphaned)
func TestDeletedInodePath(t *testing.T) {
rootNode := testDeletedIno{}
mnt, _, clean := testMount(t, &rootNode, &Options{Logger: log.New(os.Stderr, "", 0)})
defer clean()
mnt, _ := testMount(t, &rootNode, &Options{Logger: log.New(os.Stderr, "", 0)})
// Open a file handle so the kernel cannot FORGET the inode
fd, err := os.Open(mnt + "/dir")
......@@ -207,11 +204,9 @@ func (n *testDeletedIno) Getattr(ctx context.Context, f FileHandle, out *fuse.At
// We used to panic like this because inode number 1 was special:
//
// panic: using reserved ID 1 for inode number
//
func TestIno1(t *testing.T) {
rootNode := testIno1{}
mnt, _, clean := testMount(t, &rootNode, nil)
defer clean()
mnt, _ := testMount(t, &rootNode, nil)
var st syscall.Stat_t
err := syscall.Stat(mnt+"/ino1", &st)
......
......@@ -9,11 +9,15 @@ import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"sync"
"syscall"
"testing"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
type keepCacheFile struct {
......@@ -92,8 +96,8 @@ func (r *keepCacheRoot) OnAdd(ctx context.Context) {
// change content but no metadata.
func TestKeepCache(t *testing.T) {
root := &keepCacheRoot{}
mntDir, _, clean := testMount(t, root, nil)
defer clean()
mntDir, _ := testMount(t, root, nil)
c1, err := ioutil.ReadFile(mntDir + "/keep")
if err != nil {
t.Fatalf("read keep 1: %v", err)
......@@ -134,3 +138,103 @@ func TestKeepCache(t *testing.T) {
t.Errorf("nokeep read 2 got %q want read 1 %q", c2, c1)
}
}
type countingSymlink struct {
Inode
mu sync.Mutex
readCount int
data []byte
}
var _ = (NodeGetattrer)((*countingSymlink)(nil))
func (l *countingSymlink) Getattr(ctx context.Context, fh FileHandle, out *fuse.AttrOut) syscall.Errno {
l.mu.Lock()
defer l.mu.Unlock()
out.Attr.Size = uint64(len(l.data))
return 0
}
var _ = (NodeReadlinker)((*countingSymlink)(nil))
func (l *countingSymlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
l.mu.Lock()
defer l.mu.Unlock()
l.readCount++
return l.data, 0
}
func (l *countingSymlink) count() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.readCount
}
func TestSymlinkCaching(t *testing.T) {
mnt := t.TempDir()
want := "target"
link := countingSymlink{
data: []byte(want),
}
sz := len(link.data)
root := &Inode{}
dt := 10 * time.Millisecond
opts := &Options{
EntryTimeout: &dt,
AttrTimeout: &dt,
OnAdd: func(ctx context.Context) {
root.AddChild("link",
root.NewPersistentInode(ctx, &link, StableAttr{Mode: syscall.S_IFLNK}), false)
},
}
opts.Debug = testutil.VerboseTest()
opts.EnableSymlinkCaching = true
server, err := Mount(mnt, root, opts)
if err != nil {
t.Fatal(err)
}
defer server.Unmount()
for i := 0; i < 2; i++ {
if got, err := os.Readlink(mnt + "/link"); err != nil {
t.Fatal(err)
} else if got != want {
t.Fatalf("got %q want %q", got, want)
}
}
if c := link.count(); c != 1 {
t.Errorf("got %d want 1", c)
}
if errno := link.NotifyContent(0, int64(sz)); errno != 0 {
t.Fatalf("NotifyContent: %v", errno)
}
if _, err := os.Readlink(mnt + "/link"); err != nil {
t.Fatal(err)
}
if c := link.count(); c != 2 {
t.Errorf("got %d want 2", c)
}
// The actual test goes till here. The below is just to
// clarify behavior of the feature: changed attributes do not
// trigger reread, and the Attr.Size is used to truncate a
// previous read result.
link.mu.Lock()
link.data = []byte("x")
link.mu.Unlock()
time.Sleep((3 * dt) / 2)
if l, err := os.Readlink(mnt + "/link"); err != nil {
t.Fatal(err)
} else if l != want[:1] {
log.Printf("got %q want %q", l, want[:1])
}
if c := link.count(); c != 2 {
t.Errorf("got %d want 2", c)
}
}
// Copyright 2023 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"
"syscall"
"testing"
"github.com/hanwen/go-fuse/v2/fuse"
)
type dirStreamErrorNode struct {
Inode
}
var _ = (NodeReaddirer)((*dirStreamErrorNode)(nil))
func (n *dirStreamErrorNode) Readdir(ctx context.Context) (DirStream, syscall.Errno) {
return &errDirStream{}, 0
}
type errDirStream struct {
num int
}
func (ds *errDirStream) HasNext() bool {
return ds.num < 2
}
func (ds *errDirStream) Next() (fuse.DirEntry, syscall.Errno) {
ds.num++
if ds.num == 1 {
return fuse.DirEntry{
Mode: fuse.S_IFREG,
Name: "first",
Ino: 2,
}, 0
}
if ds.num == 2 {
return fuse.DirEntry{
Mode: fuse.S_IFREG,
Name: "last",
Ino: 3,
}, syscall.EKEYEXPIRED
}
panic("boom")
}
func (ds *errDirStream) Close() {
}
func TestDirStreamError(t *testing.T) {
for _, disableReaddirplus := range []bool{false, true} {
t.Run(fmt.Sprintf("disableReaddirplus=%v", disableReaddirplus),
func(t *testing.T) {
root := &dirStreamErrorNode{}
opts := Options{}
opts.DisableReadDirPlus = disableReaddirplus
mnt, _ := testMount(t, root, &opts)
ds, errno := NewLoopbackDirStream(mnt)
if errno != 0 {
t.Fatalf("NewLoopbackDirStream: %v", errno)
}
defer ds.Close()
if e, errno := ds.Next(); errno != 0 {
t.Errorf("ds.Next: %v", errno)
} else if e.Name != "first" {
t.Errorf("got %q want 'first'", e.Name)
}
if _, errno := ds.Next(); errno != syscall.EKEYEXPIRED {
t.Errorf("got errno %v, want EKEYEXPIRED", errno)
}
})
}
}
......@@ -55,8 +55,7 @@ func (f *dioFile) Open(ctx context.Context, flags uint32) (fh FileHandle, fuseFl
// this tests FOPEN_DIRECT_IO (as opposed to O_DIRECTIO)
func TestFUSEDirectIO(t *testing.T) {
root := &dioRoot{}
mntDir, server, clean := testMount(t, root, nil)
defer clean()
mntDir, server := testMount(t, root, nil)
f, err := os.Open(mntDir + "/file")
if err != nil {
......
......@@ -15,6 +15,7 @@ import (
type loopbackDirStream struct {
buf []byte
todo []byte
todoErrno syscall.Errno
// Protects fd so we can guard against double close
mu sync.Mutex
......@@ -52,7 +53,7 @@ func (ds *loopbackDirStream) Close() {
func (ds *loopbackDirStream) HasNext() bool {
ds.mu.Lock()
defer ds.mu.Unlock()
return len(ds.todo) > 0
return len(ds.todo) > 0 || ds.todoErrno != 0
}
// Like syscall.Dirent, but without the [256]byte name.
......@@ -68,6 +69,10 @@ func (ds *loopbackDirStream) Next() (fuse.DirEntry, syscall.Errno) {
ds.mu.Lock()
defer ds.mu.Unlock()
if ds.todoErrno != 0 {
return fuse.DirEntry{}, ds.todoErrno
}
// We can't use syscall.Dirent here, because it declares a
// [256]byte name, which may run beyond the end of ds.todo.
// when that happens in the race detector, it causes a panic
......@@ -99,9 +104,10 @@ func (ds *loopbackDirStream) load() syscall.Errno {
}
n, err := syscall.Getdents(ds.fd, ds.buf)
if err != nil {
return ToErrno(err)
if n < 0 {
n = 0
}
ds.todo = ds.buf[:n]
ds.todoErrno = ToErrno(err)
return OK
}
......@@ -29,7 +29,6 @@ import (
// $ ls -i1 /tmp/x/2 /tmp/x/8/6/4/2
// 2 /tmp/x/2
// 2 /tmp/x/8/6/4/2
//
type numberNode struct {
// Must embed an Inode for the struct to work as a node.
fs.Inode
......
......@@ -155,13 +155,26 @@ func (f *loopbackFile) Setattr(ctx context.Context, in *fuse.SetAttrIn, out *fus
return f.Getattr(ctx, out)
}
func (f *loopbackFile) setAttr(ctx context.Context, in *fuse.SetAttrIn) syscall.Errno {
func (f *loopbackFile) fchmod(mode uint32) syscall.Errno {
f.mu.Lock()
defer f.mu.Unlock()
return ToErrno(syscall.Fchmod(f.fd, mode))
}
func (f *loopbackFile) fchown(uid, gid int) syscall.Errno {
f.mu.Lock()
defer f.mu.Unlock()
return ToErrno(syscall.Fchown(f.fd, uid, gid))
}
func (f *loopbackFile) ftruncate(sz uint64) syscall.Errno {
return ToErrno(syscall.Ftruncate(f.fd, int64(sz)))
}
func (f *loopbackFile) setAttr(ctx context.Context, in *fuse.SetAttrIn) syscall.Errno {
var errno syscall.Errno
if mode, ok := in.GetMode(); ok {
errno = ToErrno(syscall.Fchmod(f.fd, mode))
if errno != 0 {
if errno := f.fchmod(mode); errno != 0 {
return errno
}
}
......@@ -178,8 +191,7 @@ func (f *loopbackFile) setAttr(ctx context.Context, in *fuse.SetAttrIn) syscall.
if gOk {
gid = int(gid32)
}
errno = ToErrno(syscall.Fchown(f.fd, uid, gid))
if errno != 0 {
if errno := f.fchown(uid, gid); errno != 0 {
return errno
}
}
......@@ -203,8 +215,7 @@ func (f *loopbackFile) setAttr(ctx context.Context, in *fuse.SetAttrIn) syscall.
}
if sz, ok := in.GetSize(); ok {
errno = ToErrno(syscall.Ftruncate(f.fd, int64(sz)))
if errno != 0 {
if errno := f.ftruncate(sz); errno != 0 {
return errno
}
}
......
......@@ -79,11 +79,7 @@ func TestForget(t *testing.T) {
EntryTimeout: &sec,
}
options.Debug = testutil.VerboseTest()
dir, err := ioutil.TempDir("", "TestForget")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir := t.TempDir()
rawFS := NewNodeFS(root, options)
server, err := fuse.NewServer(rawFS, dir, &options.MountOptions)
......
......@@ -511,6 +511,41 @@ func (n *Inode) Children() map[string]*Inode {
return r
}
type childEntry struct {
Name string
Inode *Inode
}
// childrenList returns the list of children of this directory Inode.
// The result is guaranteed to be stable as long as the directory did
// not change.
func (n *Inode) childrenList() []childEntry {
n.mu.Lock()
defer n.mu.Unlock()
r := make([]childEntry, 0, 2*len(n.children))
// The spec doesn't guarantee this, but as long as maps remain
// backed by hash tables, the simplest mechanism for
// randomization is picking a random start index. We undo this
// here by picking a deterministic start index again. If the
// Go runtime ever implements a memory moving GC, we might
// have to look at the keys instead.
minNode := ^uintptr(0)
minIdx := -1
for k, v := range n.children {
if p := uintptr(unsafe.Pointer(v)); p < minNode {
minIdx = len(r)
minNode = p
}
r = append(r, childEntry{Name: k, Inode: v})
}
if minIdx > 0 {
r = append(r[minIdx:], r[:minIdx]...)
}
return r
}
// Parents returns a parent of this Inode, or nil if this Inode is
// deleted or is the root
func (n *Inode) Parent() (string, *Inode) {
......
......@@ -18,11 +18,11 @@ func TestInodeParents(t *testing.T) {
// non-dupes should be stored
all := []parentData{
parentData{"foo", &ino1},
parentData{"foo2", &ino1},
parentData{"foo3", &ino1},
parentData{"foo", &ino2},
parentData{"foo", &ino3},
{"foo", &ino1},
{"foo2", &ino1},
{"foo3", &ino1},
{"foo", &ino2},
{"foo", &ino3},
}
for i, v := range all {
p.add(v)
......
......@@ -55,15 +55,10 @@ func TestInterrupt(t *testing.T) {
root := &interruptRoot{}
oneSec := time.Second
mntDir, _, clean := testMount(t, root, &Options{
mntDir, server := testMount(t, root, &Options{
EntryTimeout: &oneSec,
AttrTimeout: &oneSec,
})
defer func() {
if clean != nil {
clean()
}
}()
cmd := exec.Command("cat", mntDir+"/file")
if err := cmd.Start(); err != nil {
......@@ -74,9 +69,7 @@ func TestInterrupt(t *testing.T) {
if err := cmd.Process.Kill(); err != nil {
t.Errorf("Kill: %v", err)
}
clean()
clean = nil
server.Unmount()
if !root.child.interrupted {
t.Errorf("open request was not interrupted")
......
......@@ -70,25 +70,6 @@ type LoopbackNode struct {
}
var _ = (NodeStatfser)((*LoopbackNode)(nil))
var _ = (NodeStatfser)((*LoopbackNode)(nil))
var _ = (NodeGetattrer)((*LoopbackNode)(nil))
var _ = (NodeGetxattrer)((*LoopbackNode)(nil))
var _ = (NodeSetxattrer)((*LoopbackNode)(nil))
var _ = (NodeRemovexattrer)((*LoopbackNode)(nil))
var _ = (NodeListxattrer)((*LoopbackNode)(nil))
var _ = (NodeReadlinker)((*LoopbackNode)(nil))
var _ = (NodeOpener)((*LoopbackNode)(nil))
var _ = (NodeCopyFileRanger)((*LoopbackNode)(nil))
var _ = (NodeLookuper)((*LoopbackNode)(nil))
var _ = (NodeOpendirer)((*LoopbackNode)(nil))
var _ = (NodeReaddirer)((*LoopbackNode)(nil))
var _ = (NodeMkdirer)((*LoopbackNode)(nil))
var _ = (NodeMknoder)((*LoopbackNode)(nil))
var _ = (NodeLinker)((*LoopbackNode)(nil))
var _ = (NodeSymlinker)((*LoopbackNode)(nil))
var _ = (NodeUnlinker)((*LoopbackNode)(nil))
var _ = (NodeRmdirer)((*LoopbackNode)(nil))
var _ = (NodeRenamer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno {
s := syscall.Statfs_t{}
......@@ -107,6 +88,8 @@ func (n *LoopbackNode) path() string {
return filepath.Join(n.RootData.Path, path)
}
var _ = (NodeLookuper)((*LoopbackNode)(nil))
func (n *LoopbackNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
p := filepath.Join(n.path(), name)
......@@ -135,6 +118,8 @@ func (n *LoopbackNode) preserveOwner(ctx context.Context, path string) error {
return syscall.Lchown(path, int(caller.Uid), int(caller.Gid))
}
var _ = (NodeMknoder)((*LoopbackNode)(nil))
func (n *LoopbackNode) Mknod(ctx context.Context, name string, mode, rdev uint32, out *fuse.EntryOut) (*Inode, syscall.Errno) {
p := filepath.Join(n.path(), name)
err := syscall.Mknod(p, mode, int(rdev))
......@@ -156,6 +141,8 @@ func (n *LoopbackNode) Mknod(ctx context.Context, name string, mode, rdev uint32
return ch, 0
}
var _ = (NodeMkdirer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*Inode, syscall.Errno) {
p := filepath.Join(n.path(), name)
err := os.Mkdir(p, os.FileMode(mode))
......@@ -177,18 +164,24 @@ func (n *LoopbackNode) Mkdir(ctx context.Context, name string, mode uint32, out
return ch, 0
}
var _ = (NodeRmdirer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Rmdir(ctx context.Context, name string) syscall.Errno {
p := filepath.Join(n.path(), name)
err := syscall.Rmdir(p)
return ToErrno(err)
}
var _ = (NodeUnlinker)((*LoopbackNode)(nil))
func (n *LoopbackNode) Unlink(ctx context.Context, name string) syscall.Errno {
p := filepath.Join(n.path(), name)
err := syscall.Unlink(p)
return ToErrno(err)
}
var _ = (NodeRenamer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Rename(ctx context.Context, name string, newParent InodeEmbedder, newName string, flags uint32) syscall.Errno {
if flags&RENAME_EXCHANGE != 0 {
return n.renameExchange(name, newParent, newName)
......@@ -225,6 +218,8 @@ func (n *LoopbackNode) Create(ctx context.Context, name string, flags uint32, mo
return ch, lf, 0, 0
}
var _ = (NodeSymlinker)((*LoopbackNode)(nil))
func (n *LoopbackNode) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
p := filepath.Join(n.path(), name)
err := syscall.Symlink(target, p)
......@@ -244,6 +239,8 @@ func (n *LoopbackNode) Symlink(ctx context.Context, target, name string, out *fu
return ch, 0
}
var _ = (NodeLinker)((*LoopbackNode)(nil))
func (n *LoopbackNode) Link(ctx context.Context, target InodeEmbedder, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
p := filepath.Join(n.path(), name)
......@@ -263,6 +260,8 @@ func (n *LoopbackNode) Link(ctx context.Context, target InodeEmbedder, name stri
return ch, 0
}
var _ = (NodeReadlinker)((*LoopbackNode)(nil))
func (n *LoopbackNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
p := n.path()
......@@ -279,6 +278,8 @@ func (n *LoopbackNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
}
}
var _ = (NodeOpener)((*LoopbackNode)(nil))
func (n *LoopbackNode) Open(ctx context.Context, flags uint32) (fh FileHandle, fuseFlags uint32, errno syscall.Errno) {
flags = flags &^ syscall.O_APPEND
p := n.path()
......@@ -290,6 +291,8 @@ func (n *LoopbackNode) Open(ctx context.Context, flags uint32) (fh FileHandle, f
return lf, 0, 0
}
var _ = (NodeOpendirer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Opendir(ctx context.Context) syscall.Errno {
fd, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0755)
if err != nil {
......@@ -299,10 +302,14 @@ func (n *LoopbackNode) Opendir(ctx context.Context) syscall.Errno {
return OK
}
var _ = (NodeReaddirer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Readdir(ctx context.Context) (DirStream, syscall.Errno) {
return NewLoopbackDirStream(n.path())
}
var _ = (NodeGetattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Getattr(ctx context.Context, f FileHandle, out *fuse.AttrOut) syscall.Errno {
if f != nil {
return f.(FileGetattrer).Getattr(ctx, out)
......
//go:build darwin
// +build darwin
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
......@@ -16,18 +17,26 @@ import (
"github.com/hanwen/go-fuse/v2/internal/utimens"
)
var _ = (NodeGetxattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
return 0, syscall.ENOSYS
}
var _ = (NodeSetxattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno {
return syscall.ENOSYS
}
var _ = (NodeRemovexattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Removexattr(ctx context.Context, attr string) syscall.Errno {
return syscall.ENOSYS
}
var _ = (NodeListxattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
return 0, syscall.ENOSYS
}
......@@ -111,6 +120,8 @@ func (f *loopbackFile) utimens(a *time.Time, m *time.Time) syscall.Errno {
return ToErrno(err)
}
var _ = (NodeCopyFileRanger)((*LoopbackNode)(nil))
func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
offIn uint64, out *Inode, fhOut FileHandle, offOut uint64,
len uint64, flags uint64) (uint32, syscall.Errno) {
......
//go:build linux
// +build linux
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
......@@ -14,21 +15,29 @@ import (
"golang.org/x/sys/unix"
)
var _ = (NodeGetxattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
sz, err := unix.Lgetxattr(n.path(), attr, dest)
return uint32(sz), ToErrno(err)
}
var _ = (NodeSetxattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno {
err := unix.Lsetxattr(n.path(), attr, data, int(flags))
return ToErrno(err)
}
var _ = (NodeRemovexattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Removexattr(ctx context.Context, attr string) syscall.Errno {
err := unix.Lremovexattr(n.path(), attr)
return ToErrno(err)
}
var _ = (NodeListxattrer)((*LoopbackNode)(nil))
func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
sz, err := unix.Llistxattr(n.path(), dest)
return uint32(sz), ToErrno(err)
......@@ -69,6 +78,8 @@ func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newN
return ToErrno(unix.Renameat2(fd1, name, fd2, newName, unix.RENAME_EXCHANGE))
}
var _ = (NodeCopyFileRanger)((*LoopbackNode)(nil))
func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
offIn uint64, out *Inode, fhOut FileHandle, offOut uint64,
len uint64, flags uint64) (uint32, syscall.Errno) {
......
......@@ -22,7 +22,6 @@ import (
func TestRenameExchange(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
defer tc.Clean()
if err := os.Mkdir(tc.origDir+"/dir", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
......@@ -95,7 +94,6 @@ func TestRenameExchange(t *testing.T) {
func TestRenameNoOverwrite(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
defer tc.Clean()
if err := os.Mkdir(tc.origDir+"/dir", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
......@@ -123,7 +121,6 @@ func TestRenameNoOverwrite(t *testing.T) {
func TestXAttr(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
defer tc.Clean()
tc.writeOrig("file", "", 0644)
......@@ -188,7 +185,6 @@ func TestXAttr(t *testing.T) {
// so don't even bother. See `man 7 xattr` for more info.
func TestXAttrSymlink(t *testing.T) {
tc := newTestCase(t, nil)
defer tc.Clean()
path := tc.mntDir + "/symlink"
if err := syscall.Symlink("target/does/not/exist", path); err != nil {
......@@ -203,7 +199,6 @@ func TestXAttrSymlink(t *testing.T) {
func TestCopyFileRange(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
defer tc.Clean()
if !tc.server.KernelSettings().SupportsVersion(7, 28) {
t.Skip("need v7.28 for CopyFileRange")
......@@ -306,7 +301,7 @@ func waitMount(mnt string) error {
func TestParallelDiropsHang(t *testing.T) {
// We do NOT want to use newTestCase() here because we need to know the
// mnt path before the filesystem is mounted
dir := testutil.TempDir()
dir := t.TempDir()
orig := dir + "/orig"
mnt := dir + "/mnt"
if err := os.Mkdir(orig, 0755); err != nil {
......@@ -315,7 +310,6 @@ func TestParallelDiropsHang(t *testing.T) {
if err := os.Mkdir(mnt, 0755); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Unblock the goroutines onces the mount shows up in /proc/self/mounts
wait := make(chan struct{})
......@@ -398,6 +392,16 @@ func TestParallelDiropsHang(t *testing.T) {
}
func TestRoMount(t *testing.T) {
tc := newTestCase(t, &testOptions{ro: true})
defer tc.Clean()
newTestCase(t, &testOptions{ro: true})
}
func TestDirectMount(t *testing.T) {
opts := &testOptions{
directMount: true,
}
if os.Geteuid() == 0 {
t.Log("running as root, setting DirectMountStrict")
opts.directMountStrict = true
}
newTestCase(t, opts)
}
// Copyright 2022 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"
"io/ioutil"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/v2/fuse"
)
type maxWriteTestRoot struct {
Inode
sync.Mutex
// largest observed read size
largestRead int
// largest observed write size
largestWrite int
}
// https://github.com/torvalds/linux/blob/e2ae0d4a6b0ba461542f0fd0ba0b828658013e9f/include/linux/pagemap.h#L999
const VM_READAHEAD = 131072
var _ = (NodeOnAdder)((*maxWriteTestRoot)(nil))
func (n *maxWriteTestRoot) OnAdd(ctx context.Context) {
n.Inode.AddChild("file", n.Inode.NewInode(ctx, &maxWriteTestNode{maxWriteTestRoot: n}, StableAttr{}), false)
}
func (n *maxWriteTestRoot) resetStats() {
n.Lock()
n.largestWrite = 0
n.largestRead = 0
n.Unlock()
}
type maxWriteTestNode struct {
Inode
maxWriteTestRoot *maxWriteTestRoot
}
var _ = (NodeGetattrer)((*maxWriteTestNode)(nil))
func (n *maxWriteTestNode) Getattr(ctx context.Context, f FileHandle, out *fuse.AttrOut) syscall.Errno {
out.Size = 1024 * 1024 * 1024 // 1 GiB
return 0
}
var _ = (NodeOpener)((*maxWriteTestNode)(nil))
func (n *maxWriteTestNode) Open(ctx context.Context, flags uint32) (fh FileHandle, fuseFlags uint32, errno syscall.Errno) {
return &maxWriteTestFH{n.maxWriteTestRoot}, 0, OK
}
type maxWriteTestFH struct {
maxWriteTestRoot *maxWriteTestRoot
}
var _ = (FileReader)((*maxWriteTestFH)(nil))
func (fh *maxWriteTestFH) Read(ctx context.Context, data []byte, off int64) (fuse.ReadResult, syscall.Errno) {
fh.maxWriteTestRoot.Lock()
if fh.maxWriteTestRoot.largestRead < len(data) {
fh.maxWriteTestRoot.largestRead = len(data)
}
fh.maxWriteTestRoot.Unlock()
return fuse.ReadResultData(data), 0
}
var _ = (FileWriter)((*maxWriteTestFH)(nil))
func (fh *maxWriteTestFH) Write(ctx context.Context, data []byte, off int64) (written uint32, errno syscall.Errno) {
fh.maxWriteTestRoot.Lock()
if fh.maxWriteTestRoot.largestWrite < len(data) {
fh.maxWriteTestRoot.largestWrite = len(data)
}
fh.maxWriteTestRoot.Unlock()
return uint32(len(data)), 0
}
// TestMaxWrite checks that combinations of the MaxWrite, MaxReadAhead, max_read
// options result in the expected observed read and write sizes from the kernel.
func TestMaxWrite(t *testing.T) {
testcases := []fuse.MountOptions{
{
MaxWrite: 4 * 1024, // 4 kiB (one page) = lower limit in all Linux versions
},
{
MaxWrite: 8 * 1024,
},
{
MaxWrite: 9999, // let's see what happens if this is unaligned
},
{
MaxWrite: 64 * 1024, // 64 kiB = go-fuse default
},
{
MaxWrite: 128 * 1024, // 128 kiB = upper limit in Linux v4.19 and older
},
{
MaxWrite: 1024 * 1024, // 1 MiB = upper limit in Linux v4.20+
},
// cycle through readahead values
{
MaxWrite: 128 * 1024,
MaxReadAhead: 4 * 1024,
},
{
MaxWrite: 128 * 1024,
MaxReadAhead: 8 * 1024,
},
{
MaxWrite: 128 * 1024,
MaxReadAhead: 16 * 1024,
},
{
MaxWrite: 128 * 1024,
MaxReadAhead: 32 * 1024,
},
{
MaxWrite: 128 * 1024,
MaxReadAhead: 64 * 1024,
},
{
MaxWrite: 128 * 1024,
MaxReadAhead: 128 * 1024,
},
{
// both at default
},
{
// default MaxWrite
MaxReadAhead: 4 * 1024,
},
}
for _, tc := range testcases {
name := fmt.Sprintf("MaxWr%d.MaxRa%d", tc.MaxWrite, tc.MaxReadAhead)
t.Run(name, func(t *testing.T) {
root := &maxWriteTestRoot{}
root.resetStats()
mntDir, srv := testMount(t, root, &Options{MountOptions: tc})
readAheadWant := tc.MaxReadAhead
if readAheadWant == 0 {
readAheadWant = VM_READAHEAD
}
readAheadHave := bdiReadahead(mntDir)
if readAheadHave != readAheadWant {
t.Errorf("Readahead mismatch: have=bdiReadahead=%d want=%d", readAheadHave, readAheadWant)
}
actualMaxWrite := tc.MaxWrite
if srv.KernelSettings().Flags&fuse.CAP_MAX_PAGES == 0 && actualMaxWrite > 128*1024 {
// Kernel 4.19 and lower don't have CAP_MAX_PAGES and limit to 128 kiB.
actualMaxWrite = 128 * 1024
} else if tc.MaxWrite == 0 {
actualMaxWrite = 128 * 1024
}
// Try to make 2 MiB requests, which is more than the kernel supports, so
// we will observe the imposed limits in the actual request sizes.
buf := make([]byte, 2*1024*1024)
// Direct I/O
fdDirect, err := syscall.Open(mntDir+"/file", syscall.O_RDWR|syscall.O_DIRECT, 0600)
if err != nil {
t.Fatal(err)
}
defer syscall.Close(fdDirect)
_, err = syscall.Pwrite(fdDirect, buf, 0)
if err != nil {
t.Errorf("write failed: %v", err)
}
root.Lock()
if root.largestWrite != actualMaxWrite {
t.Errorf("Direct I/O largestWrite: have=%d, want=%d", root.largestWrite, actualMaxWrite)
}
root.Unlock()
_, err = syscall.Pread(fdDirect, buf, 0)
if err != nil {
t.Errorf("read failed: %v", err)
}
root.Lock()
if root.largestRead != actualMaxWrite {
t.Errorf("Direct I/O largestRead: have=%d, want=%d", root.largestRead, actualMaxWrite)
}
root.Unlock()
root.resetStats()
// Buffered I/O
fdBuffered, err := syscall.Open(mntDir+"/file", syscall.O_RDWR, 0600)
if err != nil {
t.Fatal(err)
}
defer syscall.Close(fdBuffered)
// Buffered read
_, err = syscall.Pread(fdBuffered, buf, 0)
if err != nil {
t.Errorf("read failed: %v", err)
}
root.Lock()
// On Linux 4.19, I get exactly tc.MaxReadAhead, while on 6.0 I also get
// larger reads up to 128 kiB. We log the results but don't expect anything.
t.Logf("Buffered I/O largestRead: have=%d", root.largestRead)
root.Unlock()
// Buffered write
_, err = syscall.Pwrite(fdBuffered, buf, 0)
if err != nil {
t.Errorf("write failed: %v", err)
}
root.Lock()
if root.largestWrite != actualMaxWrite {
t.Errorf("Buffered I/O largestWrite: have=%d, want=%d", root.largestWrite, actualMaxWrite)
}
root.Unlock()
})
}
}
// bdiReadahead extracts the readahead size (in bytes) of the filesystem at mnt from
// /sys/class/bdi/%d:%d/read_ahead_kb .
func bdiReadahead(mnt string) int {
var st syscall.Stat_t
err := syscall.Stat(mnt, &st)
if err != nil {
panic(err)
}
path := fmt.Sprintf("/sys/class/bdi/%d:%d/read_ahead_kb", unix.Major(st.Dev), unix.Minor(st.Dev))
buf, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
trimmed := strings.TrimSpace(string(buf))
val, err := strconv.Atoi(trimmed)
if err != nil {
panic(err)
}
return val * 1024
}
......@@ -7,20 +7,25 @@ package fs
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"math/rand"
"os"
"reflect"
"sync"
"syscall"
"testing"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil"
"github.com/hanwen/go-fuse/v2/posixtest"
)
func testMount(t *testing.T, root InodeEmbedder, opts *Options) (string, *fuse.Server, func()) {
func testMount(t *testing.T, root InodeEmbedder, opts *Options) (string, *fuse.Server) {
t.Helper()
mntDir := testutil.TempDir()
mntDir := t.TempDir()
if opts == nil {
opts = &Options{
FirstAutomaticIno: 1,
......@@ -32,20 +37,18 @@ func testMount(t *testing.T, root InodeEmbedder, opts *Options) (string, *fuse.S
if err != nil {
t.Fatal(err)
}
return mntDir, server, func() {
t.Cleanup(func() {
if err := server.Unmount(); err != nil {
t.Fatalf("testMount: Unmount failed: %v", err)
}
if err := syscall.Rmdir(mntDir); err != nil {
t.Errorf("testMount: Remove failed: %v", err)
}
}
})
return mntDir, server
}
func TestDefaultOwner(t *testing.T) {
want := "hello"
root := &Inode{}
mntDir, _, clean := testMount(t, root, &Options{
mntDir, _ := testMount(t, root, &Options{
FirstAutomaticIno: 1,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
......@@ -60,7 +63,6 @@ func TestDefaultOwner(t *testing.T) {
UID: 42,
GID: 43,
})
defer clean()
var st syscall.Stat_t
if err := syscall.Lstat(mntDir+"/file", &st); err != nil {
......@@ -70,10 +72,51 @@ func TestDefaultOwner(t *testing.T) {
}
}
func TestRootInode(t *testing.T) {
var rootIno uint64 = 42
root := &Inode{}
mntDir, _ := testMount(t, root, &Options{
RootStableAttr: &StableAttr{
Ino: rootIno,
Gen: 1,
},
})
var st syscall.Stat_t
if err := syscall.Lstat(mntDir, &st); err != nil {
t.Fatalf("Lstat: %v", err)
} else if st.Ino != rootIno {
t.Fatalf("Got Lstat inode %d, want %d", st.Ino, rootIno)
}
}
func TestLseekDefault(t *testing.T) {
data := []byte("hello")
root := &Inode{}
mntDir, _ := testMount(t, root, &Options{
FirstAutomaticIno: 1,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
ch := n.NewPersistentInode(
ctx,
&MemRegularFile{
Data: data,
Attr: fuse.Attr{
Mode: 0464,
},
}, StableAttr{})
n.AddChild("file.bin", ch, false)
},
})
posixtest.LseekHoleSeeksToEOF(t, mntDir)
}
func TestDataFile(t *testing.T) {
want := "hello"
root := &Inode{}
mntDir, _, clean := testMount(t, root, &Options{
mntDir, _ := testMount(t, root, &Options{
FirstAutomaticIno: 1,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
......@@ -89,7 +132,6 @@ func TestDataFile(t *testing.T) {
n.AddChild("file", ch, false)
},
})
defer clean()
var st syscall.Stat_t
if err := syscall.Lstat(mntDir+"/file", &st); err != nil {
......@@ -141,7 +183,7 @@ func TestDataFileLargeRead(t *testing.T) {
data := make([]byte, 256*1024)
rand.Read(data[:])
mntDir, _, clean := testMount(t, root, &Options{
mntDir, _ := testMount(t, root, &Options{
FirstAutomaticIno: 1,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
......@@ -157,7 +199,6 @@ func TestDataFileLargeRead(t *testing.T) {
n.AddChild("file", ch, false)
},
})
defer clean()
got, err := ioutil.ReadFile(mntDir + "/file")
if err != nil {
t.Fatalf("ReadFile: %v", err)
......@@ -184,8 +225,7 @@ func (s *SymlinkerRoot) Symlink(ctx context.Context, target, name string, out *f
func TestDataSymlink(t *testing.T) {
root := &SymlinkerRoot{}
mntDir, _, clean := testMount(t, root, nil)
defer clean()
mntDir, _ := testMount(t, root, nil)
if err := syscall.Symlink("target", mntDir+"/link"); err != nil {
t.Fatalf("Symlink: %v", err)
......@@ -197,3 +237,68 @@ func TestDataSymlink(t *testing.T) {
t.Errorf("Readlink: got %q want %q", got, want)
}
}
func TestReaddirplusParallel(t *testing.T) {
root := &Inode{}
N := 100
oneSec := time.Second
names := map[string]int64{}
mntDir, _ := testMount(t, root, &Options{
FirstAutomaticIno: 1,
EntryTimeout: &oneSec,
AttrTimeout: &oneSec,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
for i := 0; i < N; i++ {
ch := n.NewPersistentInode(
ctx,
&MemRegularFile{
Data: bytes.Repeat([]byte{'x'}, i),
},
StableAttr{})
name := fmt.Sprintf("file%04d", i)
names[name] = int64(i)
n.AddChild(name, ch, false)
}
},
})
read := func() (map[string]int64, error) {
es, err := os.ReadDir(mntDir)
if err != nil {
return nil, err
}
r := map[string]int64{}
for _, e := range es {
inf, err := e.Info()
if err != nil {
return nil, err
}
r[e.Name()] = inf.Size()
}
return r, nil
}
var wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
res, err := read()
if err != nil {
t.Errorf("readdir: %v", err)
}
if got, want := len(res), len(names); got != want {
t.Errorf("got %d want %d", got, want)
return
}
if !reflect.DeepEqual(res, names) {
t.Errorf("maps have different content")
}
}()
}
wg.Wait()
}
......@@ -55,8 +55,7 @@ func (fn *randomTypeTest) Readdir(ctx context.Context) (DirStream, syscall.Errno
func TestReaddirTypeFixup(t *testing.T) {
root := &randomTypeTest{}
mntDir, _, clean := testMount(t, root, nil)
defer clean()
mntDir, _ := testMount(t, root, nil)
f, err := os.Open(mntDir)
if err != nil {
......@@ -78,7 +77,6 @@ func TestReaddirTypeFixup(t *testing.T) {
if err != 0 {
t.Errorf("Next: %d", err)
}
t.Logf("%q: mode=0x%x", e.Name, e.Mode)
gotIsDir := (e.Mode & syscall.S_IFDIR) != 0
wantIsdir := (crc32.ChecksumIEEE([]byte(e.Name)) % 2) == 1
if gotIsDir != wantIsdir {
......
......@@ -16,8 +16,7 @@ import (
func TestReadonlyCreate(t *testing.T) {
root := &Inode{}
mntDir, _, clean := testMount(t, root, nil)
defer clean()
mntDir, _ := testMount(t, root, nil)
_, err := syscall.Creat(mntDir+"/test", 0644)
if want := syscall.EROFS; want != err {
......@@ -28,7 +27,7 @@ func TestReadonlyCreate(t *testing.T) {
func TestDefaultPermissions(t *testing.T) {
root := &Inode{}
mntDir, _, clean := testMount(t, root, &Options{
mntDir, _ := testMount(t, root, &Options{
OnAdd: func(ctx context.Context) {
dir := root.NewPersistentInode(ctx, &Inode{}, StableAttr{Mode: syscall.S_IFDIR})
file := root.NewPersistentInode(ctx, &Inode{}, StableAttr{Mode: syscall.S_IFREG})
......@@ -37,7 +36,6 @@ func TestDefaultPermissions(t *testing.T) {
root.AddChild("file", file, false)
},
})
defer clean()
for k, v := range map[string]uint32{
"dir": fuse.S_IFDIR | 0755,
......
......@@ -16,7 +16,7 @@ import (
func TestRmChildParallel(t *testing.T) {
want := "hello"
root := &Inode{}
_, _, clean := testMount(t, root, &Options{
testMount(t, root, &Options{
FirstAutomaticIno: 1,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
......@@ -53,5 +53,4 @@ func TestRmChildParallel(t *testing.T) {
wg.Wait()
},
})
defer clean()
}
......@@ -46,21 +46,21 @@ func (tc *testCase) writeOrig(path, content string, mode os.FileMode) {
}
}
func (tc *testCase) Clean() {
func (tc *testCase) clean() {
if err := tc.server.Unmount(); err != nil {
tc.Fatal(err)
}
if err := os.RemoveAll(tc.dir); 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
}
// newTestCase creates the directories `orig` and `mnt` inside a temporary
......@@ -70,7 +70,7 @@ func newTestCase(t *testing.T, opts *testOptions) *testCase {
opts = &testOptions{}
}
if opts.testDir == "" {
opts.testDir = testutil.TempDir()
opts.testDir = t.TempDir()
}
tc := &testCase{
dir: opts.testDir,
......@@ -107,7 +107,11 @@ func newTestCase(t *testing.T, opts *testOptions) *testCase {
Logger: log.New(os.Stderr, "", 0),
})
mOpts := &fuse.MountOptions{}
mOpts := &fuse.MountOptions{
DirectMount: opts.directMount,
DirectMountStrict: opts.directMountStrict,
EnableLocks: opts.enableLocks,
}
if !opts.suppressDebug {
mOpts.Debug = testutil.VerboseTest()
}
......@@ -123,12 +127,13 @@ func newTestCase(t *testing.T, opts *testOptions) *testCase {
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})
defer tc.Clean()
tc.writeOrig("file", "hello", 0644)
......@@ -162,15 +167,10 @@ func TestFileFdLeak(t *testing.T) {
attrCache: true,
entryCache: true,
})
defer func() {
if tc != nil {
tc.Clean()
}
}()
posixtest.FdLeak(t, tc.mntDir)
tc.Clean()
tc.clean()
bridge := tc.rawFS.(*rawBridge)
tc = nil
......@@ -181,7 +181,6 @@ func TestFileFdLeak(t *testing.T) {
func TestNotifyEntry(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
defer tc.Clean()
orig := tc.origDir + "/file"
fn := tc.mntDir + "/file"
......@@ -214,7 +213,6 @@ func TestNotifyEntry(t *testing.T) {
func TestReadDirStress(t *testing.T) {
tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
defer tc.Clean()
// Create 110 entries
for i := 0; i < 110; i++ {
......@@ -234,12 +232,11 @@ func TestReadDirStress(t *testing.T) {
return
}
_, err = f.Readdirnames(-1)
f.Close()
if err != nil {
t.Errorf("goroutine %d iteration %d: %v", gr, i, err)
f.Close()
return
}
f.Close()
}
}
......@@ -249,13 +246,13 @@ func TestReadDirStress(t *testing.T) {
go stress(i)
}
wg.Wait()
}
// This test is racy. If an external process consumes space while this
// runs, we may see spurious differences between the two statfs() calls.
func TestStatFs(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
defer tc.Clean()
empty := syscall.Statfs_t{}
orig := empty
......@@ -283,7 +280,6 @@ func TestGetAttrParallel(t *testing.T) {
// (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})
defer tc.Clean()
N := 100
......@@ -324,7 +320,6 @@ func TestGetAttrParallel(t *testing.T) {
func TestMknod(t *testing.T) {
tc := newTestCase(t, &testOptions{})
defer tc.Clean()
modes := map[string]uint32{
"regular": syscall.S_IFREG,
......@@ -358,8 +353,7 @@ func TestMknod(t *testing.T) {
}
func TestMknodNotSupported(t *testing.T) {
mountPoint := testutil.TempDir()
defer os.Remove(mountPoint)
mountPoint := t.TempDir()
server, err := Mount(mountPoint, &Inode{}, nil)
if err != nil {
......@@ -385,8 +379,10 @@ func TestPosix(t *testing.T) {
t.Run(nm, func(t *testing.T) {
tc := newTestCase(t, &testOptions{
suppressDebug: noisy[nm],
attrCache: true, entryCache: true})
defer tc.Clean()
attrCache: true,
entryCache: true,
enableLocks: true,
})
fn(t, tc.mntDir)
})
......@@ -414,7 +410,6 @@ func TestOpenDirectIO(t *testing.T) {
}
tc := newTestCase(t, &opts)
defer tc.Clean()
posixtest.DirectIO(t, tc.mntDir)
}
......@@ -430,7 +425,6 @@ func TestOpenDirectIO(t *testing.T) {
// 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})
defer tc.Clean()
{
old := runtime.GOMAXPROCS(100)
......@@ -591,12 +585,19 @@ func TestFsstress(t *testing.T) {
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.
......@@ -643,7 +644,6 @@ func TestFsstress(t *testing.T) {
func TestStaleHardlinks(t *testing.T) {
// Disable all caches we can disable
tc := newTestCase(t, &testOptions{attrCache: false, entryCache: false})
defer tc.Clean()
// "link0" is original file
link0 := tc.mntDir + "/link0"
......
......@@ -17,16 +17,8 @@ import (
)
func TestWindowsEmulations(t *testing.T) {
mntDir, err := ioutil.TempDir("", "ZipFS")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(mntDir)
origDir, err := ioutil.TempDir("", "ZipFS")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(origDir)
mntDir := t.TempDir()
origDir := t.TempDir()
rootData := &fs.LoopbackRoot{
NewNode: newWindowsNode,
......
......@@ -9,8 +9,11 @@ import (
"bytes"
"context"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"syscall"
"testing"
......@@ -19,6 +22,7 @@ import (
var testData = map[string]string{
"file.txt": "content",
"dir/": "",
"dir/subfile1": "content2",
"dir/subdir/subfile": "content3",
}
......@@ -27,9 +31,17 @@ func createZip(data map[string]string) []byte {
buf := &bytes.Buffer{}
zw := zip.NewWriter(buf)
for k, v := range data {
var keys []string
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fw, _ := zw.Create(k)
fw.Write([]byte(v))
d := []byte(testData[k])
if len(d) > 0 {
fw.Write(d)
}
}
zw.Close()
......@@ -59,10 +71,7 @@ func TestZipFS(t *testing.T) {
}
root := &zipRoot{zr: r}
mntDir, err := ioutil.TempDir("", "ZipFS")
if err != nil {
t.Fatal(err)
}
mntDir := t.TempDir()
server, err := fs.Mount(mntDir, root, nil)
if err != nil {
t.Fatal(err)
......@@ -70,6 +79,15 @@ func TestZipFS(t *testing.T) {
defer server.Unmount()
for k, v := range testData {
if strings.HasSuffix(k, "/") {
fi, err := os.Stat(filepath.Join(mntDir, k))
if err != nil {
t.Errorf("stat %s: %v", k, err)
} else if !fi.IsDir() {
t.Errorf("want isdir, got %v", fi)
}
continue
}
c, err := ioutil.ReadFile(filepath.Join(mntDir, k))
if err != nil {
t.Fatal(err)
......@@ -108,10 +126,7 @@ func TestZipFSOnAdd(t *testing.T) {
zr := &zipRoot{zr: r}
root := &fs.Inode{}
mnt, err := ioutil.TempDir("", "ZipFS")
if err != nil {
t.Fatal(err)
}
mnt := t.TempDir()
server, err := fs.Mount(mnt, root, &fs.Options{
OnAdd: func(ctx context.Context) {
root.AddChild("sub",
......
......@@ -107,6 +107,11 @@ func (zr *zipRoot) OnAdd(ctx context.Context) {
p = ch
}
if f.FileInfo().IsDir() {
continue
}
ch := p.NewPersistentInode(ctx, &zipFile{file: f}, fs.StableAttr{})
p.AddChild(base, ch, true)
}
......
......@@ -74,7 +74,7 @@
// see https://github.com/hanwen/go-fuse/issues/261 for an example of that
// problem.
//
// Higher level interfaces
// # Higher level interfaces
//
// As said above this packages provides way to implement filesystems in terms of
// raw FUSE protocol.
......@@ -82,7 +82,7 @@
// Package github.com/hanwen/go-fuse/v2/fs provides way to implement
// filesystems in terms of paths and/or inodes.
//
// Mount styles
// # Mount styles
//
// The NewServer() handles mounting the filesystem, which
// involves opening `/dev/fuse` and calling the
......@@ -153,12 +153,42 @@ type MountOptions struct {
// async I/O. Concurrency for synchronous I/O is not limited.
MaxBackground int
// Write size to use. If 0, use default. This number is
// capped at the kernel maximum.
// MaxWrite is the max size for read and write requests. If 0, use
// go-fuse default (currently 64 kiB).
// This number is internally capped at MAX_KERNEL_WRITE (higher values don't make
// sense).
//
// Non-direct-io reads are mostly served via kernel readahead, which is
// additionally subject to the MaxReadAhead limit.
//
// Implementation notes:
//
// There's four values the Linux kernel looks at when deciding the request size:
// * MaxWrite, passed via InitOut.MaxWrite. Limits the WRITE size.
// * max_read, passed via a string mount option. Limits the READ size.
// go-fuse sets max_read equal to MaxWrite.
// You can see the current max_read value in /proc/self/mounts .
// * MaxPages, passed via InitOut.MaxPages. In Linux 4.20 and later, the value
// can go up to 1 MiB and go-fuse calculates the MaxPages value acc.
// to MaxWrite, rounding up.
// On older kernels, the value is fixed at 128 kiB and the
// passed value is ignored. No request can be larger than MaxPages, so
// READ and WRITE are effectively capped at MaxPages.
// * MaxReadAhead, passed via InitOut.MaxReadAhead.
MaxWrite int
// Max read ahead to use. If 0, use default. This number is
// capped at the kernel maximum.
// MaxReadAhead is the max read ahead size to use. It controls how much data the
// kernel reads in advance to satisfy future read requests from applications.
// How much exactly is subject to clever heuristics in the kernel
// (see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/readahead.c?h=v6.2-rc5#n375
// if you are brave) and hence also depends on the kernel version.
//
// If 0, use kernel default. This number is capped at the kernel maximum
// (128 kiB on Linux) and cannot be larger than MaxWrite.
//
// MaxReadAhead only affects buffered reads (=non-direct-io), but even then, the
// kernel can and does send larger reads to satisfy read reqests from applications
// (up to MaxWrite or VM_READAHEAD_PAGES=128 kiB, whichever is less).
MaxReadAhead int
// If IgnoreSecurityLabels is set, all security related xattr
......@@ -193,6 +223,11 @@ type MountOptions struct {
// you must implement the GetLk/SetLk/SetLkw methods.
EnableLocks bool
// If set, the kernel caches all Readlink return values. The
// filesystem must use content notification to force the
// kernel to issue a new Readlink call.
EnableSymlinkCaching bool
// If set, ask kernel not to do automatic data cache invalidation.
// The filesystem is fully responsible for invalidating data cache.
ExplicitDataCacheControl bool
......@@ -217,10 +252,20 @@ type MountOptions struct {
// If set, fuse will first attempt to use syscall.Mount instead of
// fusermount to mount the filesystem. This will not update /etc/mtab
// but might be needed if fusermount is not available.
// Also, Server.Unmount will attempt syscall.Unmount before calling
// fusermount.
DirectMount bool
// Options passed to syscall.Mount, the default value used by fusermount
// is syscall.MS_NOSUID|syscall.MS_NODEV
// DirectMountStrict is like DirectMount but no fallback to fusermount is
// performed. If both DirectMount and DirectMountStrict are set,
// DirectMountStrict wins.
DirectMountStrict bool
// DirectMountFlags are the mountflags passed to syscall.Mount. If zero, the
// default value used by fusermount are used: syscall.MS_NOSUID|syscall.MS_NODEV.
//
// If you actually *want* zero flags, pass syscall.MS_MGC_VAL, which is ignored
// by the kernel. See `man 2 mount` for details about MS_MGC_VAL.
DirectMountFlags uintptr
// EnableAcls enables kernel ACL support.
......
......@@ -41,15 +41,26 @@ func mountDirect(mountPoint string, opts *MountOptions, ready chan<- error) (fd
source = opts.Name
}
var flags uintptr
flags |= syscall.MS_NOSUID | syscall.MS_NODEV
var flags uintptr = syscall.MS_NOSUID | syscall.MS_NODEV
if opts.DirectMountFlags != 0 {
flags = opts.DirectMountFlags
}
var st syscall.Stat_t
err = syscall.Stat(mountPoint, &st)
if err != nil {
return
}
// some values we need to pass to mount, but override possible since opts.Options comes after
// some values we need to pass to mount - we do as fusermount does.
// override possible since opts.Options comes after.
var r = []string{
fmt.Sprintf("fd=%d", fd),
"rootmode=40000",
"user_id=0",
"group_id=0",
fmt.Sprintf("rootmode=%o", st.Mode&syscall.S_IFMT),
fmt.Sprintf("user_id=%d", os.Geteuid()),
fmt.Sprintf("group_id=%d", os.Getegid()),
// match what we do with fusermount
fmt.Sprintf("max_read=%d", opts.MaxWrite),
}
r = append(r, opts.Options...)
......@@ -57,7 +68,11 @@ func mountDirect(mountPoint string, opts *MountOptions, ready chan<- error) (fd
r = append(r, "allow_other")
}
err = syscall.Mount(opts.FsName, mountPoint, "fuse."+opts.Name, opts.DirectMountFlags, strings.Join(r, ","))
if opts.Debug {
log.Printf("mountDirect: calling syscall.Mount(%q, %q, %q, %#x, %q)",
source, mountPoint, "fuse."+opts.Name, flags, strings.Join(r, ","))
}
err = syscall.Mount(source, mountPoint, "fuse."+opts.Name, flags, strings.Join(r, ","))
if err != nil {
syscall.Close(fd)
return
......@@ -125,13 +140,16 @@ func callFusermount(mountPoint string, opts *MountOptions) (fd int, err error) {
// Create a FUSE FS on the specified mount point. The returned
// mount point is always absolute.
func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, err error) {
if opts.DirectMount {
if opts.DirectMount || opts.DirectMountStrict {
fd, err := mountDirect(mountPoint, opts, ready)
if err == nil {
return fd, nil
} else if opts.Debug {
log.Printf("mount: failed to do direct mount: %s", err)
}
if opts.DirectMountStrict {
return -1, err
}
}
// Magic `/dev/fd/N` mountpoint. See the docs for NewServer() for how this
......@@ -157,12 +175,15 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e
}
func unmount(mountPoint string, opts *MountOptions) (err error) {
if opts.DirectMount {
if opts.DirectMount || opts.DirectMountStrict {
// Attempt to directly unmount, if fails fallback to fusermount method
err := syscall.Unmount(mountPoint, 0)
if err == nil {
return nil
}
if opts.DirectMountStrict {
return err
}
}
bin, err := fusermountBinary()
......
......@@ -3,8 +3,11 @@ package fuse
import (
"fmt"
"io/ioutil"
"os"
"syscall"
"testing"
"github.com/moby/sys/mountinfo"
)
// TestMountDevFd tests the special `/dev/fd/N` mountpoint syntax, where a
......@@ -98,3 +101,120 @@ func TestMountMaxWrite(t *testing.T) {
})
}
}
// mountCheckOptions mounts a defaultRawFileSystem and extracts the resulting effective
// mount options from /proc/self/mounts.
// The mount options are a comma-separated string like this:
// rw,nosuid,nodev,relatime,user_id=1026,group_id=1026
func mountCheckOptions(t *testing.T, opts MountOptions) (info mountinfo.Info) {
mnt, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatal(err)
}
fs := NewDefaultRawFileSystem()
srv, err := NewServer(fs, mnt, &opts)
if err != nil {
t.Fatal(err)
}
// Check mount options
mounts, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mnt))
if err != nil {
t.Error(err)
}
if len(mounts) != 1 {
t.Errorf("Could not find mountpoint %q in /proc/self/mountinfo", mnt)
}
orig := *mounts[0]
if testing.Verbose() {
t.Logf("full mountinfo: %#v", orig)
}
// We are only interested in some fields, as the others are arbitrary id numbers
// or contain random strings like "/tmp/TestDirectMount1126361240".
//
// What are all those fields: Look for "/proc/[pid]/mountinfo" in
// https://man7.org/linux/man-pages/man5/proc.5.html .
info = mountinfo.Info{
Options: orig.Options,
Source: orig.Source,
FSType: orig.FSType,
VFSOptions: orig.VFSOptions,
Optional: orig.Optional,
}
// server needs to run for Unmount to work
go srv.Serve()
err = srv.Unmount()
if err != nil {
t.Error(err)
}
return info
}
// TestDirectMount checks that DirectMount and DirectMountStrict work and show the
// same effective mount options in /proc/self/mounts
func TestDirectMount(t *testing.T) {
optsTable := []MountOptions{
{Debug: true},
{Debug: true, AllowOther: true},
{Debug: true, MaxWrite: 9999},
{Debug: true, FsName: "aaa"},
{Debug: true, Name: "bbb"},
{Debug: true, FsName: "ccc", Name: "ddd"},
{Debug: true, FsName: "a,b"},
{Debug: true, FsName: `a\b`},
{Debug: true, FsName: `a\,b`},
}
for _, opts := range optsTable {
// Without DirectMount - i.e. using fusermount
o1 := mountCheckOptions(t, opts)
// With DirectMount
opts.DirectMount = true
o2 := mountCheckOptions(t, opts)
if o2 != o1 {
t.Errorf(`DirectMount effective mount options mismatch:
DirectMount: %#v
fusermount: %#v`, o2, o1)
// When this already fails then DirectMountStrict will fail the same way.
// Skip it for less noise in the logs.
continue
}
if os.Geteuid() == 0 {
// With DirectMountStrict
opts.DirectMountStrict = true
o3 := mountCheckOptions(t, opts)
if o3 != o1 {
t.Errorf(`DirectMountStrict effective mount options mismatch:
DirectMountStrict: %#v
fusermount: %#v`, o3, o1)
}
}
}
}
// TestEscapedMountOption tests that fusermount doesn't exit when when using commas or backslashs in options.
// It also tests that commas or backslashs in options are correctly propagated to /proc/mounts.
func TestEscapedMountOption(t *testing.T) {
fsname := `fsname,with\,many,comm\as,and\backsl\\ashs`
opts := &MountOptions{
FsName: fsname,
}
mnt := t.TempDir()
fs := NewDefaultRawFileSystem()
srv, err := NewServer(fs, mnt, opts)
if err != nil {
t.Error(err)
}
go srv.Serve()
defer srv.Unmount()
mounts, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mnt))
if err != nil {
t.Fatal(err)
}
if len(mounts) != 1 {
t.Fatalf("Could not find mountpoint %q in /proc/self/mountinfo", mnt)
}
m := *mounts[0]
if m.Source != fsname {
t.Errorf("mountinfo(%q): got %q want %q", mnt, m.Source, fsname)
}
}
......@@ -70,6 +70,12 @@ const (
_OP_NOTIFY_DELETE = uint32(104) // protocol version 18
_OPCODE_COUNT = uint32(105)
// Constants from Linux kernel fs/fuse/fuse_i.h
// Default MaxPages value in all kernel versions
_FUSE_DEFAULT_MAX_PAGES_PER_REQ = 32
// Upper MaxPages limit in Linux v4.20+ (v4.19 and older: 32)
_FUSE_MAX_MAX_PAGES = 256
)
////////////////////////////////////////////////////////////////
......@@ -90,12 +96,14 @@ func doInit(server *Server, req *request) {
server.reqMu.Lock()
server.kernelSettings = *input
server.kernelSettings.Flags = input.Flags & (CAP_ASYNC_READ | CAP_BIG_WRITES | CAP_FILE_OPS |
CAP_READDIRPLUS | CAP_NO_OPEN_SUPPORT | CAP_PARALLEL_DIROPS)
CAP_READDIRPLUS | CAP_NO_OPEN_SUPPORT | CAP_PARALLEL_DIROPS | CAP_MAX_PAGES)
if server.opts.EnableLocks {
server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS
}
if server.opts.EnableSymlinkCaching {
server.kernelSettings.Flags |= CAP_CACHE_SYMLINKS
}
if server.opts.EnableAcl {
server.kernelSettings.Flags |= CAP_POSIX_ACL
}
......@@ -123,6 +131,11 @@ func doInit(server *Server, req *request) {
if input.Minor >= 13 {
server.setSplice()
}
// maxPages is the maximum request size we want the kernel to use, in units of
// memory pages (usually 4kiB). Linux v4.19 and older ignore this and always use
// 128kiB.
maxPages := (server.opts.MaxWrite-1)/syscall.Getpagesize() + 1 // Round up
server.reqMu.Unlock()
out := (*InitOut)(req.outData())
......@@ -134,6 +147,7 @@ func doInit(server *Server, req *request) {
MaxWrite: uint32(server.opts.MaxWrite),
CongestionThreshold: uint16(server.opts.MaxBackground * 3 / 4),
MaxBackground: uint16(server.opts.MaxBackground),
MaxPages: uint16(maxPages),
}
if server.opts.MaxReadAhead != 0 && uint32(server.opts.MaxReadAhead) < out.MaxReadAhead {
......@@ -536,6 +550,7 @@ func getHandler(o uint32) *operationHandler {
return operationHandlers[o]
}
// maximum size of all input headers
var maxInputSize uintptr
func init() {
......@@ -771,6 +786,7 @@ func init() {
_OP_READ: func(ptr unsafe.Pointer) interface{} { return (*ReadIn)(ptr) },
_OP_WRITE: func(ptr unsafe.Pointer) interface{} { return (*WriteIn)(ptr) },
_OP_READDIR: func(ptr unsafe.Pointer) interface{} { return (*ReadIn)(ptr) },
_OP_FSYNCDIR: func(ptr unsafe.Pointer) interface{} { return (*FsyncIn)(ptr) },
_OP_ACCESS: func(ptr unsafe.Pointer) interface{} { return (*AccessIn)(ptr) },
_OP_FORGET: func(ptr unsafe.Pointer) interface{} { return (*ForgetIn)(ptr) },
_OP_BATCH_FORGET: func(ptr unsafe.Pointer) interface{} { return (*_BatchForgetIn)(ptr) },
......
......@@ -6,17 +6,12 @@ package pathfs
import (
"io/ioutil"
"os"
"testing"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
func TestCopyFile(t *testing.T) {
d1 := testutil.TempDir()
defer os.RemoveAll(d1)
d2 := testutil.TempDir()
defer os.RemoveAll(d2)
d1 := t.TempDir()
d2 := t.TempDir()
fs1 := NewLoopbackFileSystem(d1)
fs2 := NewLoopbackFileSystem(d2)
......
......@@ -35,7 +35,7 @@ func (fs *ownerFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse
}
func setupOwnerTest(t *testing.T, opts *nodefs.Options) (workdir string, cleanup func()) {
wd := testutil.TempDir()
wd := t.TempDir()
opts.Debug = testutil.VerboseTest()
fs := &ownerFs{NewDefaultFileSystem()}
......@@ -50,7 +50,6 @@ func setupOwnerTest(t *testing.T, opts *nodefs.Options) (workdir string, cleanup
}
return wd, func() {
state.Unmount()
os.RemoveAll(wd)
}
}
......
......@@ -2,20 +2,23 @@
// 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 pathfs
import (
"os"
"path/filepath"
"reflect"
"syscall"
"testing"
)
func TestSysUtimensat(t *testing.T) {
symlink := "/tmp/TestSysUtimensat"
os.Remove(symlink)
dir := t.TempDir()
symlink := filepath.Join(dir, "symlink")
err := os.Symlink("/nonexisting/file", symlink)
if err != nil {
t.Fatal(err)
......
......@@ -2,6 +2,7 @@
// 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 pathfs
......@@ -15,7 +16,6 @@ import (
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
var xattrGolden = map[string][]byte{
......@@ -102,7 +102,7 @@ func (fs *XAttrTestFs) RemoveXAttr(name string, attr string, context *fuse.Conte
func xattrTestCase(t *testing.T, nm string, m map[string][]byte) (mountPoint string, cleanup func()) {
xfs := NewXAttrFs(nm, m)
mountPoint = testutil.TempDir()
mountPoint = t.TempDir()
nfs := NewPathNodeFs(xfs, nil)
state, _, err := nodefs.MountRoot(mountPoint, nfs.Root(),
......@@ -114,7 +114,6 @@ func xattrTestCase(t *testing.T, nm string, m map[string][]byte) (mountPoint str
go state.Serve()
return mountPoint, func() {
state.Unmount()
os.RemoveAll(mountPoint)
}
}
......
......@@ -77,6 +77,9 @@ var (
W_OK: "w",
R_OK: "r",
}
getAttrFlagNames = map[int64]string{
FUSE_GETATTR_FH: "FH",
}
)
func flagString(names map[int64]string, fl int64, def string) string {
......@@ -209,7 +212,7 @@ func (o *AttrOut) string() string {
// ft converts (seconds , nanoseconds) -> float(seconds)
func ft(tsec uint64, tnsec uint32) float64 {
return float64(tsec) + float64(tnsec)*1E-9
return float64(tsec) + float64(tnsec)*1e-9
}
// Returned by LOOKUP
......
......@@ -37,7 +37,7 @@ func (in *CreateIn) string() string {
}
func (in *GetAttrIn) string() string {
return fmt.Sprintf("{Fh %d}", in.Fh_)
return fmt.Sprintf("{Fh %d %s}", in.Fh_, flagString(getAttrFlagNames, int64(in.Flags_), ""))
}
func (in *MknodIn) string() string {
......
......@@ -21,13 +21,16 @@ import (
)
const (
// The kernel caps writes at 128k.
MAX_KERNEL_WRITE = 128 * 1024
// Linux v4.20+ caps requests at 1 MiB. Older kernels at 128 kiB.
MAX_KERNEL_WRITE = 1024 * 1024
// Linux kernel constant from include/uapi/linux/fuse.h
// Reads from /dev/fuse that are smaller fail with EINVAL.
_FUSE_MIN_READ_BUFFER = 8192
// defaultMaxWrite is the default value for MountOptions.MaxWrite
defaultMaxWrite = 128 * 1024 // 128 kiB
minMaxReaders = 2
maxMaxReaders = 16
)
......@@ -113,14 +116,14 @@ func (ms *Server) RecordLatencies(l LatencyMap) {
// Unmount calls fusermount -u on the mount. This has the effect of
// shutting down the filesystem. After the Server is unmounted, it
// should be discarded.
// should be discarded. This function is idempotent.
//
// Does not work when we were mounted with the magic /dev/fd/N mountpoint syntax,
// as we do not know the real mountpoint. Unmount using
//
// fusermount -u /path/to/real/mountpoint
//
/// in this case.
// in this case.
func (ms *Server) Unmount() (err error) {
if ms.mountPoint == "" {
return nil
......@@ -167,11 +170,12 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
o.MaxWrite = 0
}
if o.MaxWrite == 0 {
o.MaxWrite = 1 << 16
o.MaxWrite = defaultMaxWrite
}
if o.MaxWrite > MAX_KERNEL_WRITE {
o.MaxWrite = MAX_KERNEL_WRITE
}
if o.Name == "" {
name := fs.String()
l := len(name)
......@@ -181,12 +185,6 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
o.Name = strings.Replace(name[:l], ",", ";", -1)
}
for _, s := range o.optionsStrings() {
if strings.Contains(s, ",") {
return nil, fmt.Errorf("found ',' in option string %q", s)
}
}
maxReaders := runtime.GOMAXPROCS(0)
if maxReaders < minMaxReaders {
maxReaders = minMaxReaders
......@@ -247,6 +245,10 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
return ms, nil
}
func escape(optionValue string) string {
return strings.Replace(strings.Replace(optionValue, `\`, `\\`, -1), `,`, `\,`, -1)
}
func (o *MountOptions) optionsStrings() []string {
var r []string
r = append(r, o.Options...)
......@@ -254,13 +256,13 @@ func (o *MountOptions) optionsStrings() []string {
if o.AllowOther {
r = append(r, "allow_other")
}
if o.FsName != "" {
r = append(r, "fsname="+o.FsName)
}
if o.Name != "" {
r = append(r, "subtype="+o.Name)
}
r = append(r, fmt.Sprintf("max_read=%d", o.MaxWrite))
// OSXFUSE applies a 60-second timeout for file operations. This
// is inconsistent with how FUSE works on Linux, where operations
......@@ -269,7 +271,15 @@ func (o *MountOptions) optionsStrings() []string {
r = append(r, "daemon_timeout=0")
}
return r
// Commas and backslashs in an option need to be escaped, because
// options are separated by a comma and backslashs are used to
// escape other characters.
var rEscaped []string
for _, s := range r {
rEscaped = append(rEscaped, escape(s))
}
return rEscaped
}
// DebugData returns internal status information for debugging
......
......@@ -19,12 +19,12 @@ func (s *Server) setSplice() {
//
// This is a four-step process:
//
// 1) Splice data form fdData.Fd into the "pair1" pipe buffer --> pair1: [payload]
// 1. Splice data form fdData.Fd into the "pair1" pipe buffer --> pair1: [payload]
// Now we know the actual payload length and can
// construct the reply header
// 2) Write header into the "pair2" pipe buffer --> pair2: [header]
// 4) Splice data from "pair1" into "pair2" --> pair2: [header][payload]
// 3) Splice the data from "pair2" into /dev/fuse
// 2. Write header into the "pair2" pipe buffer --> pair2: [header]
// 4. Splice data from "pair1" into "pair2" --> pair2: [header][payload]
// 3. Splice the data from "pair2" into /dev/fuse
//
// This dance is neccessary because header and payload cannot be split across
// two splices and we cannot seek in a pipe buffer.
......
......@@ -37,7 +37,7 @@ func (fs *cacheFs) Open(name string, flags uint32, context *fuse.Context) (fuseF
}
func setupCacheTest(t *testing.T) (string, *pathfs.PathNodeFs, func()) {
dir := testutil.TempDir()
dir := t.TempDir()
os.Mkdir(dir+"/mnt", 0755)
os.Mkdir(dir+"/orig", 0755)
......@@ -65,10 +65,7 @@ func setupCacheTest(t *testing.T) (string, *pathfs.PathNodeFs, func()) {
t.Fatal("WaitMount", err)
}
return dir, pfs, func() {
err := state.Unmount()
if err == nil {
os.RemoveAll(dir)
}
state.Unmount()
}
}
......@@ -102,11 +99,15 @@ func TestFopenKeepCache(t *testing.T) {
return st
}
// XXX Linux FUSE client automatically invalidates cache of a file if it sees size change.
// As workaround we keep len(before) == len(after) to avoid that codepath.
// See https://github.com/hanwen/go-fuse/pull/273 for details.
// Without CAP_EXPLICIT_INVAL_DATA Linux FUSE client automatically
// invalidates cache of a file if it sees size change. As workaround we
// keep len(before) == len(after) to avoid that codepath.
//
// TODO use len(before) != len(after) if kernel supports precise control over data cache.
// With CAP_EXPLICIT_INVAL_DATA - even when data cache stays the same,
// but st_size changes, Linux reads file content as dcache[:st_size],
// i.e. either truncated (st_size↓) or extended (st_size↑). Here
// len(before) == len(after) also helps to avoid dealing with those
// peculiarities.
before := "before"
after := "afterX"
if len(before) != len(after) {
......@@ -133,7 +134,8 @@ func TestFopenKeepCache(t *testing.T) {
// this forces kernel client to relookup/regetattr the file and reread the attributes.
//
// this way we make sure the kernel knows updated size/mtime before we
// try to read the file next time.
// try to read the file next time, which should invalidate data cache
// if CAP_EXPLICIT_INVAL_DATA was not negotiated.
time.Sleep(100 * time.Millisecond)
_ = xstat(wd + "/mnt/file.txt")
......@@ -146,9 +148,9 @@ func TestFopenKeepCache(t *testing.T) {
t.Skipf("protocol v%d has no notify support.", minor)
}
code := pathfs.EntryNotify("", "file.txt")
code := pathfs.FileNotify("file.txt", 0, 0) // invalidate direntry and whole data cache
if !code.Ok() {
t.Errorf("EntryNotify: %v", code)
t.Errorf("FileNotify: %v", code)
}
c = xreadFile(wd + "/mnt/file.txt")
......@@ -186,8 +188,7 @@ func TestNonseekable(t *testing.T) {
fs := &nonseekFs{FileSystem: pathfs.NewDefaultFileSystem()}
fs.Length = 200 * 1024
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
nfs := pathfs.NewPathNodeFs(fs, nil)
opts := nodefs.NewOptions()
opts.Debug = testutil.VerboseTest()
......@@ -216,8 +217,7 @@ func TestNonseekable(t *testing.T) {
}
func TestGetAttrRace(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
os.Mkdir(dir+"/mnt", 0755)
os.Mkdir(dir+"/orig", 0755)
......
......@@ -52,13 +52,7 @@ func (d *DataNode) Read(_ nodefs.File, dest []byte, off int64, _ *fuse.Context)
// TestCacheControl verifies that FUSE server process can store/retrieve kernel data cache.
func TestCacheControl(t *testing.T) {
dir := testutil.TempDir()
defer func() {
err := os.Remove(dir)
if err != nil {
t.Fatal(err)
}
}()
dir := t.TempDir()
// setup a filesystem with 1 file
root := nodefs.NewDefaultNode()
......
......@@ -16,8 +16,7 @@ import (
)
func TestDefaultNodeGetAttr(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
opts := &nodefs.Options{
// Note: defaultNode.GetAttr() calling file.GetAttr() is only useful if
......
......@@ -6,7 +6,6 @@ package test
import (
"io/ioutil"
"os"
"testing"
"github.com/hanwen/go-fuse/v2/fuse"
......@@ -42,7 +41,7 @@ func defaultReadTest(t *testing.T) (root string, cleanup func()) {
}
var err error
dir := testutil.TempDir()
dir := t.TempDir()
pathfs := pathfs.NewPathNodeFs(fs, nil)
opts := nodefs.NewOptions()
opts.Debug = testutil.VerboseTest()
......@@ -57,7 +56,6 @@ func defaultReadTest(t *testing.T) (root string, cleanup func()) {
}
return dir, func() {
state.Unmount()
os.Remove(dir)
}
}
......
......@@ -34,8 +34,7 @@ func (f *flipNode) GetAttr(out *fuse.Attr, file nodefs.File, c *fuse.Context) fu
}
func TestDeleteNotify(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
root := nodefs.NewMemNodeFSRoot(dir + "/backing")
conn := nodefs.NewFileSystemConnector(root,
&nodefs.Options{PortableInodes: true})
......
......@@ -2,6 +2,7 @@
// 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
......@@ -122,8 +123,7 @@ func TestFlockInvoked(t *testing.T) {
t.Skip("flock command not found.")
}
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
opts := &nodefs.Options{
Owner: fuse.CurrentOwner(),
......@@ -187,17 +187,8 @@ func TestNoLockSupport(t *testing.T) {
t.Skip("flock command not found.")
}
tmp, err := ioutil.TempDir("", "TestNoLockSupport")
if err != nil {
t.Fatal(err)
}
mnt, err := ioutil.TempDir("", "TestNoLockSupport")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
defer os.RemoveAll(mnt)
tmp := t.TempDir()
mnt := t.TempDir()
opts := &nodefs.Options{
Owner: fuse.CurrentOwner(),
......
......@@ -143,7 +143,7 @@ func NewFile() *MutableDataFile {
}
func setupFAttrTest(t *testing.T, fs pathfs.FileSystem) (dir string, clean func()) {
dir = testutil.TempDir()
dir = t.TempDir()
nfs := pathfs.NewPathNodeFs(fs, nil)
opts := nodefs.NewOptions()
opts.Debug = testutil.VerboseTest()
......@@ -161,8 +161,6 @@ func setupFAttrTest(t *testing.T, fs pathfs.FileSystem) (dir string, clean func(
clean = func() {
if err := state.Unmount(); err != nil {
t.Errorf("cleanup: Unmount: %v", err)
} else {
os.RemoveAll(dir)
}
}
......
......@@ -17,7 +17,6 @@ import (
"golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
var enableOverlayfsTest bool
......@@ -147,11 +146,9 @@ func TestOverlayfs(t *testing.T) {
tc.Mkdir(tc.origSubdir, 0777)
tc.WriteFile(filepath.Join(tc.origSubdir, testfile), content, 0700)
tmpMergedDir := testutil.TempDir()
defer os.RemoveAll(tmpMergedDir)
tmpWorkDir := testutil.TempDir()
defer os.RemoveAll(tmpWorkDir)
tmpUpperDir := testutil.TempDir()
tmpMergedDir := t.TempDir()
tmpWorkDir := t.TempDir()
tmpUpperDir := t.TempDir()
defer os.RemoveAll(tmpUpperDir)
if err := unix.Mount("overlay", tmpMergedDir, "overlay", 0,
fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", tc.mnt, tmpUpperDir, tmpWorkDir)); err != nil {
......
......@@ -77,7 +77,7 @@ func NewTestCase(t *testing.T) *testCase {
const subdir string = "subdir"
var err error
tc.tmpDir = testutil.TempDir()
tc.tmpDir = t.TempDir()
tc.orig = tc.tmpDir + "/orig"
tc.mnt = tc.tmpDir + "/mnt"
......@@ -125,7 +125,6 @@ func (tc *testCase) Cleanup() {
if err != nil {
tc.tester.Fatalf("Unmount failed: %v", err)
}
os.RemoveAll(tc.tmpDir)
}
func (tc *testCase) rootNode() *nodefs.Inode {
......@@ -774,8 +773,7 @@ func TestNonVerboseFStatFs(t *testing.T) {
}
func TestOriginalIsSymlink(t *testing.T) {
tmpDir := testutil.TempDir()
defer os.RemoveAll(tmpDir)
tmpDir := t.TempDir()
orig := tmpDir + "/orig"
err := os.Mkdir(orig, 0755)
if err != nil {
......
......@@ -15,7 +15,6 @@ import (
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
func TestMountOnExisting(t *testing.T) {
......@@ -171,8 +170,7 @@ func TestDeletedUnmount(t *testing.T) {
}
func TestDefaultNodeMount(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
root := nodefs.NewDefaultNode()
s, conn, err := nodefs.MountRoot(dir, root, nil)
if err != nil {
......@@ -198,8 +196,7 @@ func TestDefaultNodeMount(t *testing.T) {
}
func TestLiveness(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
root := nodefs.NewDefaultNode()
s, _, err := nodefs.MountRoot(dir, root, nil)
if err != nil {
......
......@@ -28,13 +28,7 @@ func (d *truncatableFile) Truncate(file nodefs.File, size uint64, context *fuse.
// TestNilFileTruncation verifies that the FUSE server process does not
// crash when file truncation is performed on nil file handles.
func TestNilFileTruncation(t *testing.T) {
dir := testutil.TempDir()
defer func() {
err := os.Remove(dir)
if err != nil {
t.Fatal(err)
}
}()
dir := t.TempDir()
root := nodefs.NewDefaultNode()
opts := nodefs.NewOptions()
......
......@@ -11,7 +11,6 @@ import (
"fmt"
"io/ioutil"
"log"
"os"
"testing"
"golang.org/x/sync/errgroup"
......@@ -45,7 +44,6 @@ func (r *tRoot) Lookup(out *fuse.Attr, name string, fctx *fuse.Context) (*nodefs
return node.Inode(), st
}
// verifyFileRead verifies that file @path has content == dataOK.
func verifyFileRead(path string, dataOK string) error {
v, err := ioutil.ReadFile(path)
......@@ -59,13 +57,7 @@ func verifyFileRead(path string, dataOK string) error {
}
func TestNodeParallelLookup(t *testing.T) {
dir := testutil.TempDir()
defer func() {
err := os.Remove(dir)
if err != nil {
t.Fatal(err)
}
}()
dir := t.TempDir()
root := &tRoot{
Node: nodefs.NewDefaultNode(),
......@@ -99,7 +91,7 @@ func TestNodeParallelLookup(t *testing.T) {
}()
// the test will deadlock if the client cannot issue several lookups simultaneously
if srv.KernelSettings().Flags & fuse.CAP_PARALLEL_DIROPS == 0 {
if srv.KernelSettings().Flags&fuse.CAP_PARALLEL_DIROPS == 0 {
t.Skip("Kernel serializes dir lookups")
}
......@@ -110,10 +102,10 @@ func TestNodeParallelLookup(t *testing.T) {
defer cancel()
wg, ctx := errgroup.WithContext(ctx0)
wg.Go(func() error {
return verifyFileRead(dir + "/hello", "abc")
return verifyFileRead(dir+"/hello", "abc")
})
wg.Go(func() error {
return verifyFileRead(dir + "/world", "def")
return verifyFileRead(dir+"/world", "def")
})
// wait till both threads queue into Lookup
......
......@@ -61,7 +61,7 @@ func (n *rootNode) Lookup(out *fuse.Attr, name string, context *fuse.Context) (*
}
func TestUpdateNode(t *testing.T) {
dir := testutil.TempDir()
dir := t.TempDir()
root := &rootNode{
Node: nodefs.NewDefaultNode(),
backing: map[string]string{"a": "aaa"},
......
......@@ -6,7 +6,6 @@ package test
import (
"io/ioutil"
"os"
"sync/atomic"
"testing"
......@@ -51,13 +50,7 @@ func (d *NoFileNode) Open(flags uint32, context *fuse.Context) (nodefs.File, fus
}
func TestNoFile(t *testing.T) {
dir := testutil.TempDir()
defer func() {
err := os.Remove(dir)
if err != nil {
t.Fatal(err)
}
}()
dir := t.TempDir()
// setup a filesystem with 2 files:
//
......
......@@ -78,7 +78,7 @@ type NotifyTest struct {
func NewNotifyTest(t *testing.T) *NotifyTest {
me := &NotifyTest{}
me.fs = newNotifyFs()
me.dir = testutil.TempDir()
me.dir = t.TempDir()
entryTTL := 100 * time.Millisecond
opts := &nodefs.Options{
EntryTimeout: entryTTL,
......
......@@ -34,7 +34,7 @@ func (fs *umaskFS) Mkdir(name string, mode uint32, context *fuse.Context) (code
}
func TestUmask(t *testing.T) {
tmpDir := testutil.TempDir()
tmpDir := t.TempDir()
orig := tmpDir + "/orig"
mnt := tmpDir + "/mnt"
......
......@@ -2,12 +2,12 @@
// 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"
"path/filepath"
"syscall"
"testing"
......@@ -39,8 +39,7 @@ func (n *xattrChildNode) GetXAttr(attr string, context *fuse.Context) ([]byte, f
}
func TestDefaultXAttr(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
root := &xattrNode{
Node: nodefs.NewDefaultNode(),
......@@ -69,8 +68,7 @@ func TestDefaultXAttr(t *testing.T) {
}
func TestEmptyXAttr(t *testing.T) {
dir := testutil.TempDir()
defer os.RemoveAll(dir)
dir := t.TempDir()
root := &xattrNode{
Node: nodefs.NewDefaultNode(),
......
......@@ -2,6 +2,7 @@
// 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
......
......@@ -2,8 +2,9 @@ module github.com/hanwen/go-fuse/v2
require (
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
github.com/moby/sys/mountinfo v0.6.2
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
)
go 1.13
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
......@@ -16,6 +16,7 @@ import (
// Called by TestLoopbackFileUtimens and TestLoopbackFileSystemUtimens.
//
// Parameters:
//
// path ........ path to the backing file
// utimensFn ... Utimens() function that acts on the backing file
func TestLoopbackUtimens(t *testing.T, path string, utimensFn func(atime *time.Time, mtime *time.Time) fuse.Status) {
......
// Copyright 2016 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 testutil
import (
"io/ioutil"
"log"
"runtime"
"strings"
)
// TempDir creates a temporary directory that includes the name of the
// testcase. Panics if there was an I/O problem creating the directory.
func TempDir() string {
frames := make([]uintptr, 10) // at least 1 entry needed
n := runtime.Callers(1, frames)
lastName := ""
for _, pc := range frames[:n] {
f := runtime.FuncForPC(pc)
name := f.Name()
i := strings.LastIndex(name, ".")
if i >= 0 {
name = name[i+1:]
}
if strings.HasPrefix(name, "Test") {
lastName = name
}
}
dir, err := ioutil.TempDir("", lastName)
if err != nil {
log.Panicf("TempDir(%s): %v", lastName, err)
}
return dir
}
......@@ -335,7 +335,7 @@ var _ = (fs.NodeReaddirer)((*unionFSNode)(nil))
func (n *unionFSNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
root := n.root()
markers := map[string]struct{}{delDirHash: struct{}{}}
markers := map[string]struct{}{delDirHash: {}}
// ignore error: assume no markers
root.allMarkers(markers)
......@@ -346,18 +346,29 @@ func (n *unionFSNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno)
// deepest root first.
readRoot(root.roots[len(root.roots)-i-1], dir, names)
}
result := make([]fuse.DirEntry, 0, len(names))
result := make([]fuse.DirEntry, 0, 2*len(names))
maxIdx := -1
maxName := ""
for nm, mode := range names {
marker := filePathHash(filepath.Join(dir, nm))
if _, ok := markers[marker]; ok {
continue
}
if nm > maxName {
maxName = nm
maxIdx = len(result)
}
result = append(result, fuse.DirEntry{
Name: nm,
Mode: mode,
})
}
if len(result) > 0 {
result = append(result[maxIdx:], result[:maxIdx]...)
}
return fs.NewListDirStream(result), 0
}
......
......@@ -33,12 +33,11 @@ func (tc *testCase) Clean() {
tc.server.Unmount()
tc.server = nil
}
os.RemoveAll(tc.dir)
}
func newTestCase(t *testing.T, populate bool) *testCase {
t.Helper()
dir := testutil.TempDir()
dir := t.TempDir()
dirs := []string{"ro", "rw", "mnt"}
if populate {
dirs = append(dirs, "ro/dir")
......
......@@ -16,6 +16,7 @@ import (
"testing"
"github.com/hanwen/go-fuse/v2/fuse"
"golang.org/x/sys/unix"
)
// All holds a map of all test functions
......@@ -32,6 +33,7 @@ var All = map[string]func(*testing.T, string){
"ParallelFileOpen": ParallelFileOpen,
"Link": Link,
"LinkUnlinkRename": LinkUnlinkRename,
"LseekHoleSeeksToEOF": LseekHoleSeeksToEOF,
"RenameOverwriteDestNoExist": RenameOverwriteDestNoExist,
"RenameOverwriteDestExist": RenameOverwriteDestExist,
"RenameOpenDir": RenameOpenDir,
......@@ -41,6 +43,8 @@ var All = map[string]func(*testing.T, string){
"OpenAt": OpenAt,
"Fallocate": Fallocate,
"DirSeek": DirSeek,
"FcntlFlockSetLk": FcntlFlockSetLk,
"FcntlFlockLocksFile": FcntlFlockLocksFile,
}
func DirectIO(t *testing.T, mnt string) {
......@@ -631,3 +635,88 @@ func Fallocate(t *testing.T, mnt string) {
fi.Size())
}
}
func FcntlFlockSetLk(t *testing.T, mnt string) {
for i, cmd := range []int{syscall.F_SETLK, syscall.F_SETLKW} {
filename := mnt + fmt.Sprintf("/file%d", i)
f1, err := os.Create(filename)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f1.Close()
wlk := syscall.Flock_t{
Type: syscall.F_WRLCK,
Start: 0,
Len: 0,
}
if err := syscall.FcntlFlock(f1.Fd(), cmd, &wlk); err != nil {
t.Fatalf("FcntlFlock failed: %v", err)
}
f2, err := os.OpenFile(filename, os.O_RDWR, 0766)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f2.Close()
lk := syscall.Flock_t{}
if err := syscall.FcntlFlock(f2.Fd(), unix.F_OFD_GETLK, &lk); err != nil {
t.Errorf("FcntlFlock failed: %v", err)
}
if lk.Type != syscall.F_WRLCK {
t.Errorf("got lk.Type=%v, want %v", lk.Type, syscall.F_WRLCK)
}
}
}
func FcntlFlockLocksFile(t *testing.T, mnt string) {
filename := mnt + "/test"
f1, err := os.Create(filename)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f1.Close()
wlk := syscall.Flock_t{
Type: syscall.F_WRLCK,
Start: 0,
Len: 0,
}
if err := syscall.FcntlFlock(f1.Fd(), syscall.F_SETLK, &wlk); err != nil {
t.Fatalf("FcntlFlock failed: %v", err)
}
f2, err := os.OpenFile(filename, os.O_RDWR, 0766)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f2.Close()
rlk := syscall.Flock_t{
Type: syscall.F_RDLCK,
Start: 0,
Len: 0,
}
if err := syscall.FcntlFlock(f2.Fd(), syscall.F_SETLK, &rlk); err != syscall.EAGAIN {
t.Errorf("FcntlFlock returned %v, expected EAGAIN", err)
}
}
func LseekHoleSeeksToEOF(t *testing.T, mnt string) {
fn := filepath.Join(mnt, "file.bin")
content := bytes.Repeat([]byte("abcxyz\n"), 1024)
if err := ioutil.WriteFile(fn, content, 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
fd, err := syscall.Open(fn, syscall.O_RDONLY, 0644)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer syscall.Close(fd)
off, err := unix.Seek(fd, int64(len(content)/2), unix.SEEK_HOLE)
if err != nil {
t.Fatalf("Seek: %v", err)
} else if off != int64(len(content)) {
t.Errorf("got offset %d, want %d", off, len(content))
}
}
......@@ -4,8 +4,6 @@
package splice
import ()
func (p *Pair) LoadFromAt(fd uintptr, sz int, off int64) (int, error) {
panic("not implemented")
return 0, nil
......
......@@ -19,7 +19,7 @@ const testTtl = 100 * time.Millisecond
func setupMzfs(t *testing.T) (mountPoint string, state *fuse.Server, cleanup func()) {
root := &MultiZipFs{}
mountPoint = testutil.TempDir()
mountPoint = t.TempDir()
dt := testTtl
opts := &fs.Options{
......@@ -34,7 +34,6 @@ func setupMzfs(t *testing.T) (mountPoint string, state *fuse.Server, cleanup fun
}
return mountPoint, server, func() {
server.Unmount()
os.RemoveAll(mountPoint)
}
}
......
......@@ -67,8 +67,7 @@ func TestTar(t *testing.T) {
root := &tarRoot{rc: &addClose{buf}}
mnt := testutil.TempDir()
defer os.Remove(mnt)
mnt := t.TempDir()
opts := &fs.Options{}
opts.Debug = testutil.VerboseTest()
s, err := fs.Mount(mnt, root, opts)
......@@ -95,12 +94,12 @@ func TestTar(t *testing.T) {
}
} else if strings.HasSuffix(k, "/") {
if got, want := st.Mode, uint32(syscall.S_IFDIR|0464); got != want {
if got, want := uint32(st.Mode), uint32(syscall.S_IFDIR|0464); got != want {
t.Errorf("dir %q: got mode %o, want %o", k, got, want)
}
} else {
if got, want := st.Mode, uint32(syscall.S_IFREG|0464); got != want {
if got, want := uint32(st.Mode), uint32(syscall.S_IFREG|0464); got != want {
t.Errorf("entry %q, got mode %o, want %o", k, got, want)
}
......
......@@ -33,14 +33,13 @@ func setupZipfs(t *testing.T) (mountPoint string, cleanup func()) {
t.Fatalf("NewArchiveFileSystem failed: %v", err)
}
mountPoint = testutil.TempDir()
mountPoint = t.TempDir()
opts := &fs.Options{}
opts.Debug = testutil.VerboseTest()
server, err := fs.Mount(mountPoint, root, opts)
return mountPoint, func() {
server.Unmount()
os.RemoveAll(mountPoint)
}
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment