Commit 43d5f40d authored by Kirill Smelkov's avatar Kirill Smelkov

Merge branch 'y/nodefs-cancel' into t

* y/nodefs-cancel: (60 commits)
  Make ReadDirPlus configurable
  all.bash: use /bin/bash explicitly
  fuse: Add release flag constants
  Fix sync.Pool leak
  README: make godoc links point to v2 instead of ancient v1.0.0
  Ports commit fee50bf0 to poll_darwin.go in order to allow read-only mounts on Darwin as well.
  Make OSXFUSE handle timeouts the same way as Linux
  Update to fuse 7.12
  example: remove unionfs examples
  unionfs: remove library
  posixtest: provide more detail for failing ReadDir test
  fuse: decode MKNOD result in debug output
  fuse: fix mounting with MaxWrite < 8kiB
  fuse: add MaxPages to INIT debug output
  fs: update oldParent.children for exchange rename
  Fix IOCTL to return the error allowed by overlayfs
  fuse: prefer fusermount3 over fusermount; add debug output
  Replace defunct travis ci with github actions
  Rewrite defunct all.bash
  fuse: move parseFuseFd() to unbreak darwin build
  ...
parents 51ae026e 643585d7
name: CI
on:
push:
pull_request:
schedule:
- cron: '0 12 * * *' # Every day noon UTC
jobs:
build:
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
# Don't cancel everything when one Go version fails
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Install Go ${{ matrix.go }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Make "git describe" work
# CI platform specific setup steps happen here
- run: sudo apt-get install -qq fuse3 libssl-dev libfuse-dev
# Actual test steps are in all.bash
- run: ./all.bash
...@@ -14,3 +14,6 @@ example/bulkstat/bulkstat ...@@ -14,3 +14,6 @@ example/bulkstat/bulkstat
example/zipfs/zipfs example/zipfs/zipfs
# test binaries # test binaries
*.test *.test
benchmark/bulkstat.bin
benchmark/bulkstat/main
benchmark/cstatfs
sudo: required
# Ubuntu 18.04 "Bionic", https://docs.travis-ci.com/user/reference/bionic/
# Kernel 5.0.0-1026-gcp
dist: bionic
language: go
go_import_path: github.com/hanwen/go-fuse
go:
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x
- master
matrix:
fast_finish: true
allow_failures:
- go: master
before_install:
- sudo apt-get install -qq pkg-config fuse
- sudo modprobe fuse
- sudo chmod 666 /dev/fuse
- sudo chown root:$USER /etc/fuse.conf
install:
- go get -t ./...
- go get -t -race ./...
# Travis CI has a no-output-timeout of 10 minutes.
# Set "go test -timeout" lower so we get proper backtraces
# on a hung test.
# The tests sometimes hang in a way that "go test -timeout"
# does not work anymore. Use the external "timeout" command
# as backup, triggering 1 minute later.
script:
- set -e # fail fast
- timeout -s QUIT -k 10s 90s go test -failfast -timeout 1m -p 1 -v ./fs
- timeout -s QUIT -k 10s 6m go test -failfast -timeout 5m -p 1 -v ./...
- set +e # restore
...@@ -16,5 +16,6 @@ Patrick Crosby <pcrosby@gmail.com> ...@@ -16,5 +16,6 @@ Patrick Crosby <pcrosby@gmail.com>
Paul Jolly <paul@myitcv.org.uk> Paul Jolly <paul@myitcv.org.uk>
Paul Warren <paul.warren@emc.com> Paul Warren <paul.warren@emc.com>
Shayan Pooya <shayan@arista.com> Shayan Pooya <shayan@arista.com>
Tommy Lindgren <tommy.lindgren@gmail.com>
Valient Gough <vgough@pobox.com> Valient Gough <vgough@pobox.com>
Yongwoo Park <nnnlife@gmail.com> Yongwoo Park <nnnlife@gmail.com>
# GO-FUSE # GO-FUSE
[![Build Status](https://travis-ci.org/hanwen/go-fuse.svg?branch=master)](https://travis-ci.org/hanwen/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) [![GoDoc](https://godoc.org/github.com/hanwen/go-fuse?status.svg)](https://godoc.org/github.com/hanwen/go-fuse/v2)
Go native bindings for the FUSE kernel module. Go native bindings for the FUSE kernel module.
You should import and use You should import and use
[github.com/hanwen/go-fuse/fs](https://godoc.org/github.com/hanwen/go-fuse/fs) [github.com/hanwen/go-fuse/v2/fs](https://godoc.org/github.com/hanwen/go-fuse/v2/fs)
library. It follows the wire protocol closely, but provides library. It follows the wire protocol closely, but provides
convenient abstractions for building both node and path based file convenient abstractions for building both node and path based file
systems systems
Older, deprecated APIs are available at Older, deprecated APIs are available at
[github.com/hanwen/go-fuse/fuse/pathfs](https://godoc.org/github.com/hanwen/go-fuse/fuse/pathfs) [github.com/hanwen/go-fuse/fuse/pathfs](https://godoc.org/github.com/hanwen/go-fuse/v2/fuse/pathfs)
and and
[github.com/hanwen/go-fuse/fuse/nodefs](https://godoc.org/github.com/hanwen/go-fuse/fuse/nodefs). [github.com/hanwen/go-fuse/fuse/nodefs](https://godoc.org/github.com/hanwen/go-fuse/v2/fuse/nodefs).
## Comparison with other FUSE libraries ## Comparison with other FUSE libraries
The FUSE library gained a new, cleaned-up API during a rewrite The FUSE library gained a new, cleaned-up API during a rewrite
completed in 2019. Find extensive documentation completed in 2019. Find extensive documentation
[here](https://godoc.org/github.com/hanwen/go-fuse/). [here](https://godoc.org/github.com/hanwen/go-fuse/v2).
Further highlights of this library is Further highlights of this library is
......
#!/bin/sh #!/bin/bash
set -eu set -eux
for d in fuse fuse/nodefs fuse/pathfs fuse/test zipfs unionfs \ # Everything must compile on Linux
example/hello example/loopback example/zipfs \ go build ./...
example/multizip example/unionfs example/memfs \
example/autounionfs example/statfs ; \
do
go build -o /dev/null github.com/hanwen/go-fuse/${d}
done
# Not everything compiles on MacOS (try GOOS=darwin go build ./...).
# But our key packages should.
GOOS=darwin go build ./fuse/... ./fs/... ./example/loopback/...
for d in fuse zipfs unionfs fuse/test # Run the tests. Why the flags:
do # -timeout 5m ... Get a backtrace on a hung test before the CI system kills us
( # -p 1 .......... Run tests serially, which also means we get live output
cd $d # instead of per-package buffering.
# -count 1 ...... Disable result caching, so we can see flakey tests
# Make sure it compiles on all platforms. go test -timeout 5m -p 1 -count 1 ./...
for GOOS in darwin linux ; do
export GOOS
go test -c -i github.com/hanwen/go-fuse/$d
done
echo "go test github.com/hanwen/go-fuse/$d"
go test github.com/hanwen/go-fuse/$d
echo "go test -race github.com/hanwen/go-fuse/$d"
go test -race github.com/hanwen/go-fuse/$d
)
done
for target in "clean" "install" ; do
for d in fuse fuse/nodefs fuse/pathfs fuse/test zipfs unionfs \
example/hello example/loopback example/zipfs \
example/multizip example/unionfs example/memfs \
example/autounionfs example/statfs ; \
do
if test "${target}" = "install" && test "${d}" = "fuse/test"; then
continue
fi
echo "go ${target} github.com/hanwen/go-fuse/${d}"
go ${target} github.com/hanwen/go-fuse/${d}
done
done
make -C benchmark make -C benchmark
for d in benchmark go test ./benchmark -test.bench '.*' -test.cpu 1,2
do
go test github.com/hanwen/go-fuse/benchmark -test.bench '.*' -test.cpu 1,2
done
// Copyright 2021 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 benchmark
import (
"fmt"
"os"
"os/exec"
"testing"
"github.com/hanwen/go-fuse/v2/internal/testutil"
"golang.org/x/sync/errgroup"
)
func BenchmarkGoFuseRead(b *testing.B) {
fs := &readFS{}
wd, clean := setupFs(fs, b.N)
defer clean()
jobs := 32
blockSize := 64 * 1024
cmds := make([]*exec.Cmd, jobs)
for i := 0; i < jobs; i++ {
cmds[i] = exec.Command("dd",
fmt.Sprintf("if=%s/foo.txt", wd),
"iflag=direct",
"of=/dev/null",
fmt.Sprintf("bs=%d", blockSize),
fmt.Sprintf("count=%d", b.N))
if testutil.VerboseTest() {
cmds[i].Stdout = os.Stdout
cmds[i].Stderr = os.Stderr
}
}
b.SetBytes(int64(jobs * blockSize))
b.ReportAllocs()
b.ResetTimer()
var eg errgroup.Group
for i := 0; i < jobs; i++ {
i := i
eg.Go(func() error {
return cmds[i].Run()
})
}
if err := eg.Wait(); err != nil {
b.Fatalf("dd failed: %v", err)
}
b.StopTimer()
}
// Copyright 2021 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 benchmark
import (
"context"
"syscall"
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
const fileSize = 2 << 60
// readFS is a filesystem that always and immediately returns zeros on read
// operations. Useful when benchmarking the raw throughput with go-fuse.
type readFS struct {
fs.Inode
}
var _ = (fs.NodeLookuper)((*readFS)(nil))
func (n *readFS) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
sattr := fs.StableAttr{Mode: fuse.S_IFREG}
return n.NewInode(ctx, &readFS{}, sattr), fs.OK
}
var _ = (fs.NodeGetattrer)((*readFS)(nil))
func (n *readFS) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
out.Size = fileSize
out.SetTimeout(time.Hour)
return fs.OK
}
var _ = (fs.NodeOpener)((*readFS)(nil))
func (n *readFS) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
return &readFS{}, fuse.FOPEN_DIRECT_IO, fs.OK
}
var _ = (fs.FileReader)((*readFS)(nil))
func (n *readFS) Read(ctx context.Context, dest []byte, offset int64) (fuse.ReadResult, syscall.Errno) {
return fuse.ReadResultData(dest), fs.OK
}
...@@ -129,7 +129,7 @@ func BenchmarkGoFuseStat(b *testing.B) { ...@@ -129,7 +129,7 @@ func BenchmarkGoFuseStat(b *testing.B) {
threads := runtime.GOMAXPROCS(0) threads := runtime.GOMAXPROCS(0)
if err := TestingBOnePass(b, threads, fileList, wd); err != nil { if err := TestingBOnePass(b, threads, fileList, wd); err != nil {
log.Fatalf("TestingBOnePass %v8", err) b.Fatalf("TestingBOnePass %v8", err)
} }
} }
...@@ -252,6 +252,8 @@ func BenchmarkCFuseThreadedStat(b *testing.B) { ...@@ -252,6 +252,8 @@ func BenchmarkCFuseThreadedStat(b *testing.B) {
f.Close() f.Close()
mountPoint := testutil.TempDir() mountPoint := testutil.TempDir()
defer os.RemoveAll(mountPoint)
cmd := exec.Command(wd+"/cstatfs", cmd := exec.Command(wd+"/cstatfs",
"-o", "-o",
"entry_timeout=0.0,attr_timeout=0.0,ac_attr_timeout=0.0,negative_timeout=0.0", "entry_timeout=0.0,attr_timeout=0.0,ac_attr_timeout=0.0,negative_timeout=0.0",
...@@ -274,6 +276,6 @@ func BenchmarkCFuseThreadedStat(b *testing.B) { ...@@ -274,6 +276,6 @@ func BenchmarkCFuseThreadedStat(b *testing.B) {
os.Lstat(mountPoint) os.Lstat(mountPoint)
threads := runtime.GOMAXPROCS(0) threads := runtime.GOMAXPROCS(0)
if err := TestingBOnePass(b, threads, fileList, mountPoint); err != nil { if err := TestingBOnePass(b, threads, fileList, mountPoint); err != nil {
log.Fatalf("TestingBOnePass %v", err) b.Fatalf("TestingBOnePass %v", err)
} }
} }
// 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 main
import (
"flag"
"fmt"
"os"
"time"
"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/unionfs"
)
func main() {
debug := flag.Bool("debug", false, "debug on")
hardlinks := flag.Bool("hardlinks", false, "support hardlinks")
delcache_ttl := flag.Float64("deletion_cache_ttl", 5.0, "Deletion cache TTL in seconds.")
branchcache_ttl := flag.Float64("branchcache_ttl", 5.0, "Branch cache TTL in seconds.")
deldirname := flag.String(
"deletion_dirname", "GOUNIONFS_DELETIONS", "Directory name to use for deletions.")
hide_readonly_link := flag.Bool("hide_readonly_link", true,
"Hides READONLY link from the top mountpoints. "+
"Enabled by default.")
portableInodes := flag.Bool("portable-inodes", false,
"Use sequential 32-bit inode numbers.")
flag.Parse()
if len(flag.Args()) < 2 {
fmt.Println("Usage:\n main MOUNTPOINT BASEDIR")
os.Exit(2)
}
ufsOptions := unionfs.UnionFsOptions{
DeletionCacheTTL: time.Duration(*delcache_ttl * float64(time.Second)),
BranchCacheTTL: time.Duration(*branchcache_ttl * float64(time.Second)),
DeletionDirName: *deldirname,
}
options := unionfs.AutoUnionFsOptions{
UnionFsOptions: ufsOptions,
Options: nodefs.Options{
EntryTimeout: time.Second,
AttrTimeout: time.Second,
NegativeTimeout: time.Second,
Owner: fuse.CurrentOwner(),
Debug: *debug,
},
UpdateOnMount: true,
PathNodeFsOptions: pathfs.PathNodeFsOptions{
ClientInodes: *hardlinks,
},
HideReadonly: *hide_readonly_link,
}
fsOpts := nodefs.Options{
PortableInodes: *portableInodes,
Debug: *debug,
}
gofs := unionfs.NewAutoUnionFs(flag.Arg(1), options)
pathfs := pathfs.NewPathNodeFs(gofs, &pathfs.PathNodeFsOptions{
Debug: *debug,
})
state, _, err := nodefs.MountRoot(flag.Arg(0), pathfs.Root(), &fsOpts)
if err != nil {
fmt.Printf("Mount fail: %v\n", err)
os.Exit(1)
}
state.Serve()
time.Sleep(1 * time.Second)
}
// 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 main
import (
"flag"
"fmt"
"log"
"os"
"time"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/unionfs"
)
func main() {
debug := flag.Bool("debug", false, "debug on")
portable := flag.Bool("portable", false, "use 32 bit inodes")
entry_ttl := flag.Float64("entry_ttl", 1.0, "fuse entry cache TTL.")
negative_ttl := flag.Float64("negative_ttl", 1.0, "fuse negative entry cache TTL.")
delcache_ttl := flag.Float64("deletion_cache_ttl", 5.0, "Deletion cache TTL in seconds.")
branchcache_ttl := flag.Float64("branchcache_ttl", 5.0, "Branch cache TTL in seconds.")
deldirname := flag.String(
"deletion_dirname", "GOUNIONFS_DELETIONS", "Directory name to use for deletions.")
flag.Parse()
if len(flag.Args()) < 2 {
fmt.Println("Usage:\n unionfs MOUNTPOINT RW-DIRECTORY RO-DIRECTORY ...")
os.Exit(2)
}
ufsOptions := unionfs.UnionFsOptions{
DeletionCacheTTL: time.Duration(*delcache_ttl * float64(time.Second)),
BranchCacheTTL: time.Duration(*branchcache_ttl * float64(time.Second)),
DeletionDirName: *deldirname,
}
ufs, err := unionfs.NewUnionFsFromRoots(flag.Args()[1:], &ufsOptions, true)
if err != nil {
log.Fatal("Cannot create UnionFs", err)
os.Exit(1)
}
nodeFs := pathfs.NewPathNodeFs(ufs, &pathfs.PathNodeFsOptions{ClientInodes: true})
mOpts := nodefs.Options{
EntryTimeout: time.Duration(*entry_ttl * float64(time.Second)),
AttrTimeout: time.Duration(*entry_ttl * float64(time.Second)),
NegativeTimeout: time.Duration(*negative_ttl * float64(time.Second)),
PortableInodes: *portable,
Debug: *debug,
}
mountState, _, err := nodefs.MountRoot(flag.Arg(0), nodeFs.Root(), &mOpts)
if err != nil {
log.Fatal("Mount fail:", err)
}
mountState.Serve()
}
// Copyright 2020 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.
// This is main program driver for a loopback filesystem that emulates
// windows semantics (no delete/rename on opened files.)
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"path"
"sync"
"syscall"
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
// Release decreases the open count. The kernel doesn't wait with
// returning from close(), so if the caller is too quick to
// unlink/rename after calling close(), this may still trigger EBUSY.
// Kludge around this by sleeping for a bit before we check business.
var delay = flag.Duration("delay", 10*time.Microsecond,
"wait this long before checking business")
// WindowsNode is a loopback FS node keeping track of open counts.
type WindowsNode struct {
// WindowsNode inherits most functionality from LoopbackNode.
fs.LoopbackNode
mu sync.Mutex
openCount int
}
func (n *WindowsNode) increment() {
n.mu.Lock()
defer n.mu.Unlock()
n.openCount++
}
func (n *WindowsNode) decrement() {
n.mu.Lock()
defer n.mu.Unlock()
n.openCount--
}
var _ = (fs.NodeOpener)((*WindowsNode)(nil))
func (n *WindowsNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
fh, flags, errno := n.LoopbackNode.Open(ctx, flags)
if errno == 0 {
n.increment()
}
return fh, flags, errno
}
var _ = (fs.NodeCreater)((*WindowsNode)(nil))
func (n *WindowsNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
inode, fh, flags, errno := n.LoopbackNode.Create(ctx, name, flags, mode, out)
if errno == 0 {
wn := inode.Operations().(*WindowsNode)
wn.increment()
}
return inode, fh, flags, errno
}
var _ = (fs.NodeReleaser)((*WindowsNode)(nil))
func (n *WindowsNode) Release(ctx context.Context, f fs.FileHandle) syscall.Errno {
n.decrement()
if fr, ok := f.(fs.FileReleaser); ok {
return fr.Release(ctx)
}
return 0
}
func isBusy(parent *fs.Inode, name string) bool {
time.Sleep(*delay)
if ch := parent.GetChild(name); ch != nil {
if wn, ok := ch.Operations().(*WindowsNode); ok {
wn.mu.Lock()
defer wn.mu.Unlock()
if wn.openCount > 0 {
return true
}
}
}
return false
}
var _ = (fs.NodeUnlinker)((*WindowsNode)(nil))
func (n *WindowsNode) Unlink(ctx context.Context, name string) syscall.Errno {
if isBusy(n.EmbeddedInode(), name) {
return syscall.EBUSY
}
return n.LoopbackNode.Unlink(ctx, name)
}
var _ = (fs.NodeRenamer)((*WindowsNode)(nil))
func (n *WindowsNode) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno {
if isBusy(n.EmbeddedInode(), name) || isBusy(newParent.EmbeddedInode(), newName) {
return syscall.EBUSY
}
return n.LoopbackNode.Rename(ctx, name, newParent, newName, flags)
}
func newWindowsNode(rootData *fs.LoopbackRoot, _ *fs.Inode, _ string, _ *syscall.Stat_t) fs.InodeEmbedder {
n := &WindowsNode{
LoopbackNode: fs.LoopbackNode{
RootData: rootData,
},
}
return n
}
func main() {
log.SetFlags(log.Lmicroseconds)
debug := flag.Bool("debug", false, "print debugging messages.")
flag.Parse()
if flag.NArg() < 2 {
fmt.Printf("usage: %s MOUNTPOINT ORIGINAL\n", path.Base(os.Args[0]))
fmt.Printf("\noptions:\n")
flag.PrintDefaults()
os.Exit(2)
}
orig := flag.Arg(1)
rootData := &fs.LoopbackRoot{
NewNode: newWindowsNode,
Path: orig,
}
sec := time.Second
opts := &fs.Options{
AttrTimeout: &sec,
EntryTimeout: &sec,
}
opts.Debug = *debug
opts.MountOptions.Options = append(opts.MountOptions.Options, "fsname="+orig)
opts.MountOptions.Name = "winfs"
opts.NullPermissions = true
server, err := fs.Mount(flag.Arg(0), newWindowsNode(rootData, nil, "", nil), opts)
if err != nil {
log.Fatalf("Mount fail: %v\n", err)
}
fmt.Println("Mounted!")
server.Wait()
}
This diff is collapsed.
...@@ -201,3 +201,40 @@ func (n *testDeletedIno) Getattr(ctx context.Context, f FileHandle, out *fuse.At ...@@ -201,3 +201,40 @@ func (n *testDeletedIno) Getattr(ctx context.Context, f FileHandle, out *fuse.At
// Otherwise EILSEQ // Otherwise EILSEQ
return syscall.EILSEQ return syscall.EILSEQ
} }
// TestIno1 tests that inode number 1 is allowed.
//
// 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()
var st syscall.Stat_t
err := syscall.Stat(mnt+"/ino1", &st)
if err != nil {
t.Fatal(err)
}
if st.Ino != 1 {
t.Errorf("wrong inode number: want=1 have=%d", st.Ino)
}
}
type testIno1 struct {
Inode
}
func (fn *testIno1) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
if name != "ino1" {
return nil, syscall.ENOENT
}
stable := StableAttr{
Mode: syscall.S_IFREG,
Ino: 1,
}
child := fn.NewInode(ctx, &testIno1{}, stable)
return child, 0
}
...@@ -55,12 +55,26 @@ func (ds *loopbackDirStream) HasNext() bool { ...@@ -55,12 +55,26 @@ func (ds *loopbackDirStream) HasNext() bool {
return len(ds.todo) > 0 return len(ds.todo) > 0
} }
// Like syscall.Dirent, but without the [256]byte name.
type dirent struct {
Ino uint64
Off int64
Reclen uint16
Type uint8
Name [1]uint8 // align to 4 bytes for 32 bits.
}
func (ds *loopbackDirStream) Next() (fuse.DirEntry, syscall.Errno) { func (ds *loopbackDirStream) Next() (fuse.DirEntry, syscall.Errno) {
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()
de := (*syscall.Dirent)(unsafe.Pointer(&ds.todo[0]))
nameBytes := ds.todo[unsafe.Offsetof(syscall.Dirent{}.Name):de.Reclen] // 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
// "converted pointer straddles multiple allocations"
de := (*dirent)(unsafe.Pointer(&ds.todo[0]))
nameBytes := ds.todo[unsafe.Offsetof(dirent{}.Name):de.Reclen]
ds.todo = ds.todo[de.Reclen:] ds.todo = ds.todo[de.Reclen:]
// After the loop, l contains the index of the first '\0'. // After the loop, l contains the index of the first '\0'.
......
...@@ -17,7 +17,9 @@ import ( ...@@ -17,7 +17,9 @@ import (
) )
// NewLoopbackFile creates a FileHandle out of a file descriptor. All // NewLoopbackFile creates a FileHandle out of a file descriptor. All
// operations are implemented. // operations are implemented. When using the Fd from a *os.File, call
// syscall.Dup() on the fd, to avoid os.File's finalizer from closing
// the file descriptor.
func NewLoopbackFile(fd int) FileHandle { func NewLoopbackFile(fd int) FileHandle {
return &loopbackFile{fd: fd} return &loopbackFile{fd: fd}
} }
......
...@@ -112,7 +112,7 @@ func TestForget(t *testing.T) { ...@@ -112,7 +112,7 @@ func TestForget(t *testing.T) {
bridge := rawFS.(*rawBridge) bridge := rawFS.(*rawBridge)
bridge.mu.Lock() bridge.mu.Lock()
l := len(bridge.nodes) l := len(bridge.kernelNodeIds)
bridge.mu.Unlock() bridge.mu.Unlock()
if l != 1 { if l != 1 {
t.Fatalf("got %d live nodes, want 1", l) t.Fatalf("got %d live nodes, want 1", l)
......
This diff is collapsed.
// Copyright 2021 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
// inodeParents stores zero or more parents of an Inode,
// remembering which one is the most recent.
//
// No internal locking: the caller is responsible for preventing
// concurrent access.
type inodeParents struct {
// newest is the most-recently add()'ed parent.
// nil when we don't have any parents.
newest *parentData
// other are parents in addition to the newest.
// nil or empty when we have <= 1 parents.
other map[parentData]struct{}
}
// add adds a parent to the store.
func (p *inodeParents) add(n parentData) {
// one and only parent
if p.newest == nil {
p.newest = &n
}
// already known as `newest`
if *p.newest == n {
return
}
// old `newest` gets displaced into `other`
if p.other == nil {
p.other = make(map[parentData]struct{})
}
p.other[*p.newest] = struct{}{}
// new parent becomes `newest` (possibly moving up from `other`)
delete(p.other, n)
p.newest = &n
}
// get returns the most recent parent
// or nil if there is no parent at all.
func (p *inodeParents) get() *parentData {
return p.newest
}
// all returns all known parents
// or nil if there is no parent at all.
func (p *inodeParents) all() []parentData {
count := p.count()
if count == 0 {
return nil
}
out := make([]parentData, 0, count)
out = append(out, *p.newest)
for i := range p.other {
out = append(out, i)
}
return out
}
func (p *inodeParents) delete(n parentData) {
// We have zero parents, so we can't delete any.
if p.newest == nil {
return
}
// If it's not the `newest` it must be in `other` (or nowhere).
if *p.newest != n {
delete(p.other, n)
return
}
// We want to delete `newest`, but there is no other to replace it.
if len(p.other) == 0 {
p.newest = nil
return
}
// Move random entry from `other` over `newest`.
var i parentData
for i = range p.other {
p.newest = &i
break
}
delete(p.other, i)
}
func (p *inodeParents) clear() {
p.newest = nil
p.other = nil
}
func (p *inodeParents) count() int {
if p.newest == nil {
return 0
}
return 1 + len(p.other)
}
type parentData struct {
name string
parent *Inode
}
package fs
import (
"testing"
)
func TestInodeParents(t *testing.T) {
var p inodeParents
var ino1, ino2, ino3 Inode
// empty store should be empty without panicing
if count := p.count(); count != 0 {
t.Error(count)
}
if p.all() != nil {
t.Error("empty store should return nil but did not")
}
// non-dupes should be stored
all := []parentData{
parentData{"foo", &ino1},
parentData{"foo2", &ino1},
parentData{"foo3", &ino1},
parentData{"foo", &ino2},
parentData{"foo", &ino3},
}
for i, v := range all {
p.add(v)
if count := p.count(); count != i+1 {
t.Errorf("want=%d have=%d", i+1, count)
}
last := p.get()
if *last != v {
t.Error("get did not give us last-known parent")
}
}
// adding dupes should not cause the count to increase, but
// must cause get() to return the most recently added dupe.
for _, v := range all {
p.add(v)
if count := p.count(); count != len(all) {
t.Errorf("want=%d have=%d", len(all), count)
}
last := p.get()
if *last != v {
t.Error("get did not give us last-known parent")
}
}
all2 := p.all()
if len(all) != len(all2) {
t.Errorf("want=%d have=%d", len(all), len(all2))
}
}
package fs
import (
"syscall"
"testing"
)
func TestInodeIsDir(t *testing.T) {
cases := []struct {
mode uint32
dir bool
}{
{syscall.S_IFBLK, false},
{syscall.S_IFCHR, false},
{syscall.S_IFDIR, true},
{syscall.S_IFIFO, false},
{syscall.S_IFLNK, false},
{syscall.S_IFREG, false},
{syscall.S_IFSOCK, false},
}
var i Inode
for _, c := range cases {
i.stableAttr.Mode = c.mode
if i.IsDir() != c.dir {
t.Errorf("wrong result for case %#v", c)
}
}
}
This diff is collapsed.
...@@ -16,23 +16,23 @@ import ( ...@@ -16,23 +16,23 @@ import (
"github.com/hanwen/go-fuse/v2/internal/utimens" "github.com/hanwen/go-fuse/v2/internal/utimens"
) )
func (n *loopbackNode) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { func (n *LoopbackNode) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
return 0, syscall.ENOSYS return 0, syscall.ENOSYS
} }
func (n *loopbackNode) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { func (n *LoopbackNode) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno {
return syscall.ENOSYS return syscall.ENOSYS
} }
func (n *loopbackNode) Removexattr(ctx context.Context, attr string) syscall.Errno { func (n *LoopbackNode) Removexattr(ctx context.Context, attr string) syscall.Errno {
return syscall.ENOSYS return syscall.ENOSYS
} }
func (n *loopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
return 0, syscall.ENOSYS return 0, syscall.ENOSYS
} }
func (n *loopbackNode) renameExchange(name string, newparent *loopbackNode, newName string) syscall.Errno { func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
return syscall.ENOSYS return syscall.ENOSYS
} }
...@@ -111,7 +111,7 @@ func (f *loopbackFile) utimens(a *time.Time, m *time.Time) syscall.Errno { ...@@ -111,7 +111,7 @@ func (f *loopbackFile) utimens(a *time.Time, m *time.Time) syscall.Errno {
return ToErrno(err) return ToErrno(err)
} }
func (n *loopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle, func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
offIn uint64, out *Inode, fhOut FileHandle, offOut uint64, offIn uint64, out *Inode, fhOut FileHandle, offOut uint64,
len uint64, flags uint64) (uint32, syscall.Errno) { len uint64, flags uint64) (uint32, syscall.Errno) {
return 0, syscall.ENOSYS return 0, syscall.ENOSYS
......
...@@ -8,38 +8,40 @@ package fs ...@@ -8,38 +8,40 @@ package fs
import ( import (
"context" "context"
"path/filepath"
"syscall" "syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func (n *loopbackNode) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { func (n *LoopbackNode) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
sz, err := unix.Lgetxattr(n.path(), attr, dest) sz, err := unix.Lgetxattr(n.path(), attr, dest)
return uint32(sz), ToErrno(err) return uint32(sz), ToErrno(err)
} }
func (n *loopbackNode) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { func (n *LoopbackNode) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno {
err := unix.Lsetxattr(n.path(), attr, data, int(flags)) err := unix.Lsetxattr(n.path(), attr, data, int(flags))
return ToErrno(err) return ToErrno(err)
} }
func (n *loopbackNode) Removexattr(ctx context.Context, attr string) syscall.Errno { func (n *LoopbackNode) Removexattr(ctx context.Context, attr string) syscall.Errno {
err := unix.Lremovexattr(n.path(), attr) err := unix.Lremovexattr(n.path(), attr)
return ToErrno(err) return ToErrno(err)
} }
func (n *loopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
sz, err := unix.Llistxattr(n.path(), dest) sz, err := unix.Llistxattr(n.path(), dest)
return uint32(sz), ToErrno(err) return uint32(sz), ToErrno(err)
} }
func (n *loopbackNode) renameExchange(name string, newparent *loopbackNode, newName string) syscall.Errno { func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
fd1, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0) fd1, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0)
if err != nil { if err != nil {
return ToErrno(err) return ToErrno(err)
} }
defer syscall.Close(fd1) defer syscall.Close(fd1)
fd2, err := syscall.Open(newparent.path(), syscall.O_DIRECTORY, 0) p2 := filepath.Join(n.RootData.Path, newparent.EmbeddedInode().Path(nil))
fd2, err := syscall.Open(p2, syscall.O_DIRECTORY, 0)
defer syscall.Close(fd2) defer syscall.Close(fd2)
if err != nil { if err != nil {
return ToErrno(err) return ToErrno(err)
...@@ -52,22 +54,22 @@ func (n *loopbackNode) renameExchange(name string, newparent *loopbackNode, newN ...@@ -52,22 +54,22 @@ func (n *loopbackNode) renameExchange(name string, newparent *loopbackNode, newN
// Double check that nodes didn't change from under us. // Double check that nodes didn't change from under us.
inode := &n.Inode inode := &n.Inode
if inode.Root() != inode && inode.StableAttr().Ino != n.root().idFromStat(&st).Ino { if inode.Root() != inode && inode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY return syscall.EBUSY
} }
if err := syscall.Fstat(fd2, &st); err != nil { if err := syscall.Fstat(fd2, &st); err != nil {
return ToErrno(err) return ToErrno(err)
} }
newinode := &newparent.Inode newinode := newparent.EmbeddedInode()
if newinode.Root() != newinode && newinode.StableAttr().Ino != n.root().idFromStat(&st).Ino { if newinode.Root() != newinode && newinode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY return syscall.EBUSY
} }
return ToErrno(unix.Renameat2(fd1, name, fd2, newName, unix.RENAME_EXCHANGE)) return ToErrno(unix.Renameat2(fd1, name, fd2, newName, unix.RENAME_EXCHANGE))
} }
func (n *loopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle, func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
offIn uint64, out *Inode, fhOut FileHandle, offOut uint64, offIn uint64, out *Inode, fhOut FileHandle, offOut uint64,
len uint64, flags uint64) (uint32, syscall.Errno) { len uint64, flags uint64) (uint32, syscall.Errno) {
lfIn, ok := fhIn.(*loopbackFile) lfIn, ok := fhIn.(*loopbackFile)
......
...@@ -75,6 +75,22 @@ func TestRenameExchange(t *testing.T) { ...@@ -75,6 +75,22 @@ func TestRenameExchange(t *testing.T) {
if !reflect.DeepEqual(after2, before1) { if !reflect.DeepEqual(after2, before1) {
t.Errorf("after2, before1: %#v, %#v", after2, before1) t.Errorf("after2, before1: %#v, %#v", after2, before1)
} }
root := tc.loopback.EmbeddedInode().Root()
ino1 := root.GetChild("file")
if ino1 == nil {
t.Fatalf("root.GetChild(%q): null inode", "file")
}
ino2 := root.GetChild("dir").GetChild("file")
if ino2 == nil {
t.Fatalf("dir.GetChild(%q): null inode", "file")
}
if ino1.StableAttr().Ino != after1.Ino {
t.Errorf("got inode %d for %q, want %d", ino1.StableAttr().Ino, "file", after1.Ino)
}
if ino2.StableAttr().Ino != after2.Ino {
t.Errorf("got inode %d for %q want %d", ino2.StableAttr().Ino, "dir/file", after2.Ino)
}
} }
func TestRenameNoOverwrite(t *testing.T) { func TestRenameNoOverwrite(t *testing.T) {
......
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fs
import (
"context"
"fmt"
"sync"
"testing"
"github.com/hanwen/go-fuse/v2/fuse"
)
func TestRmChildParallel(t *testing.T) {
want := "hello"
root := &Inode{}
_, _, clean := testMount(t, root, &Options{
FirstAutomaticIno: 1,
OnAdd: func(ctx context.Context) {
n := root.EmbeddedInode()
var wg sync.WaitGroup
var nms []string
for i := 0; i < 100; i++ {
nms = append(nms, fmt.Sprint(i))
}
for _, nm := range nms {
wg.Add(1)
go func(nm string) {
ch := n.NewPersistentInode(
ctx,
&MemRegularFile{
Data: []byte(want),
Attr: fuse.Attr{
Mode: 0464,
},
},
StableAttr{})
n.AddChild(nm, ch, false)
wg.Done()
}(nm)
}
for _, nm := range nms {
wg.Add(1)
go func(nm string) {
n.RmChild(nm)
wg.Done()
}(nm)
}
wg.Wait()
},
})
defer clean()
}
...@@ -5,12 +5,19 @@ ...@@ -5,12 +5,19 @@
package fs package fs
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"sort"
"strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"testing" "testing"
"time" "time"
...@@ -97,6 +104,7 @@ func newTestCase(t *testing.T, opts *testOptions) *testCase { ...@@ -97,6 +104,7 @@ func newTestCase(t *testing.T, opts *testOptions) *testCase {
tc.rawFS = NewNodeFS(tc.loopback, &Options{ tc.rawFS = NewNodeFS(tc.loopback, &Options{
EntryTimeout: entryDT, EntryTimeout: entryDT,
AttrTimeout: attrDT, AttrTimeout: attrDT,
Logger: log.New(os.Stderr, "", 0),
}) })
mOpts := &fuse.MountOptions{} mOpts := &fuse.MountOptions{}
...@@ -349,6 +357,24 @@ func TestMknod(t *testing.T) { ...@@ -349,6 +357,24 @@ func TestMknod(t *testing.T) {
} }
} }
func TestMknodNotSupported(t *testing.T) {
mountPoint := testutil.TempDir()
defer os.Remove(mountPoint)
server, err := Mount(mountPoint, &Inode{}, nil)
if err != nil {
t.Fatalf("cannot mount: %v", err)
}
defer server.Unmount()
name := filepath.Join(mountPoint, "foo")
if got, want := syscall.Mknod(name, syscall.S_IFREG|0755, (8<<8)|0), syscall.ENOTSUP; got != want {
t.Fatalf("mknod: got %v, want %v", got, want)
}
}
func TestPosix(t *testing.T) { func TestPosix(t *testing.T) {
noisy := map[string]bool{ noisy := map[string]bool{
"ParallelFileOpen": true, "ParallelFileOpen": true,
...@@ -392,6 +418,264 @@ func TestOpenDirectIO(t *testing.T) { ...@@ -392,6 +418,264 @@ func TestOpenDirectIO(t *testing.T) {
posixtest.DirectIO(t, tc.mntDir) posixtest.DirectIO(t, tc.mntDir)
} }
// TestFsstress is loosely modeled after xfstest's fsstress. It performs rapid
// parallel removes / creates / readdirs. Coupled with inode reuse, this test
// used to deadlock go-fuse quite quickly.
//
// Note: Run as
//
// TMPDIR=/var/tmp go test -run TestFsstress
//
// to make sure the backing filesystem is ext4. /tmp is tmpfs on modern Linux
// distributions, and tmpfs does not reuse inode numbers, hiding the problem.
func TestFsstress(t *testing.T) {
tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
defer tc.Clean()
{
old := runtime.GOMAXPROCS(100)
defer runtime.GOMAXPROCS(old)
}
const concurrency = 10
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
// operations taking 1 path argument
ops1 := map[string]func(string) error{
"mkdir": func(p string) error { return syscall.Mkdir(p, 0700) },
"rmdir": func(p string) error { return syscall.Rmdir(p) },
"mknod_reg": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFREG, 0) },
"remove": os.Remove,
"unlink": syscall.Unlink,
"mknod_sock": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFSOCK, 0) },
"mknod_fifo": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFIFO, 0) },
"mkfifo": func(p string) error { return syscall.Mkfifo(p, 0700) },
"symlink": func(p string) error { return syscall.Symlink("foo", p) },
"creat": func(p string) error {
fd, err := syscall.Open(p, syscall.O_CREAT|syscall.O_EXCL, 0700)
if err == nil {
syscall.Close(fd)
}
return err
},
}
// operations taking 2 path arguments
ops2 := map[string]func(string, string) error{
"rename": syscall.Rename,
"link": syscall.Link,
}
type opStats struct {
ok *int64
fail *int64
hung *int64
}
stats := make(map[string]opStats)
// pathN() returns something like /var/tmp/TestFsstress/TestFsstress.4
pathN := func(n int) string {
return fmt.Sprintf("%s/%s.%d", tc.mntDir, t.Name(), n)
}
opLoop := func(k string, n int) {
defer wg.Done()
op := ops1[k]
for {
p := pathN(1)
atomic.AddInt64(stats[k].hung, 1)
err := op(p)
atomic.AddInt64(stats[k].hung, -1)
if err != nil {
atomic.AddInt64(stats[k].fail, 1)
} else {
atomic.AddInt64(stats[k].ok, 1)
}
select {
case <-ctx.Done():
return
default:
}
}
}
op2Loop := func(k string, n int) {
defer wg.Done()
op := ops2[k]
n2 := (n + 1) % concurrency
for {
p1 := pathN(n)
p2 := pathN(n2)
atomic.AddInt64(stats[k].hung, 1)
err := op(p1, p2)
atomic.AddInt64(stats[k].hung, -1)
if err != nil {
atomic.AddInt64(stats[k].fail, 1)
} else {
atomic.AddInt64(stats[k].ok, 1)
}
select {
case <-ctx.Done():
return
default:
}
}
}
readdirLoop := func(k string) {
defer wg.Done()
for {
atomic.AddInt64(stats[k].hung, 1)
f, err := os.Open(tc.mntDir)
if err != nil {
panic(err)
}
_, err = f.Readdir(0)
if err != nil {
atomic.AddInt64(stats[k].fail, 1)
} else {
atomic.AddInt64(stats[k].ok, 1)
}
f.Close()
atomic.AddInt64(stats[k].hung, -1)
select {
case <-ctx.Done():
return
default:
}
}
}
// prepare stats map
var allOps []string
for k := range ops1 {
allOps = append(allOps, k)
}
for k := range ops2 {
allOps = append(allOps, k)
}
allOps = append(allOps, "readdir")
for _, k := range allOps {
var i1, i2, i3 int64
stats[k] = opStats{ok: &i1, fail: &i2, hung: &i3}
}
// spawn worker goroutines
for i := 0; i < concurrency; i++ {
for k := range ops1 {
wg.Add(1)
go opLoop(k, i)
}
for k := range ops2 {
wg.Add(1)
go op2Loop(k, i)
}
}
{
k := "readdir"
wg.Add(1)
go readdirLoop(k)
}
// spawn ls loop
//
// An external "ls" loop has a destructive effect that I am unable to
// reproduce through in-process operations.
if strings.ContainsAny(tc.mntDir, "'\\") {
// But let's not enable shell injection.
log.Panicf("shell injection attempt? mntDir=%q", tc.mntDir)
}
// --color=always enables xattr lookups for extra stress
cmd := exec.Command("bash", "-c", "while true ; do ls -l --color=always '"+tc.mntDir+"'; done")
err := cmd.Start()
if err != nil {
t.Fatal(err)
}
defer cmd.Process.Kill()
// Run the test for 1 second. If it deadlocks, it usually does within 20ms.
time.Sleep(1 * time.Second)
cancel()
// waitTimeout waits for the waitgroup for the specified max timeout.
// Returns true if waiting timed out.
waitTimeout := func(wg *sync.WaitGroup, timeout time.Duration) bool {
c := make(chan struct{})
go func() {
defer close(c)
wg.Wait()
}()
select {
case <-c:
return false // completed normally
case <-time.After(timeout):
return true // timed out
}
}
if waitTimeout(&wg, time.Second) {
t.Errorf("timeout waiting for goroutines to exit (deadlocked?)")
}
// Print operation statistics
var keys []string
for k := range stats {
keys = append(keys, k)
}
sort.Strings(keys)
t.Logf("Operation statistics:")
for _, k := range keys {
v := stats[k]
t.Logf("%10s: %5d ok, %6d fail, %2d hung", k, *v.ok, *v.fail, *v.hung)
}
}
// TestStaleHardlinks creates a lot of hard links and deletes them again
// behind the back of the loopback fs. Then opens the original file.
//
// Fails at the moment. Core of the problem:
//
// 18:41:50.796468 rx 136: LOOKUP n1 ["link0"] 6b
// 18:41:50.796489 tx 136: OK, {n2 g1 tE=0s tA=0s {M0100600 SZ=0 L=1 1026:1026 B0*4096 i0:269663 A 1616348510.793212 M 1616348510.793212 C 1616348510.795212}}
// 18:41:50.796535 rx 138: OPEN n2 {O_RDONLY,0x8000}
// 18:41:50.796557 tx 138: 2=no such file or directory, {Fh 0 }
func TestStaleHardlinks(t *testing.T) {
// Disable all caches we can disable
tc := newTestCase(t, &testOptions{attrCache: false, entryCache: false})
defer tc.Clean()
// "link0" is original file
link0 := tc.mntDir + "/link0"
if fd, err := syscall.Creat(link0, 0600); err != nil {
t.Fatal(err)
} else {
syscall.Close(fd)
}
// Create hardlinks via mntDir
for i := 1; i < 20; i++ {
linki := fmt.Sprintf(tc.mntDir+"/link%d", i)
if err := syscall.Link(link0, linki); err != nil {
t.Fatal(err)
}
}
// Delete hardlinks via origDir (behind loopback fs's back)
for i := 1; i < 20; i++ {
linki := fmt.Sprintf(tc.origDir+"/link%d", i)
if err := syscall.Unlink(linki); err != nil {
t.Fatal(err)
}
}
// Try to open link0 via mntDir
fd, err := syscall.Open(link0, syscall.O_RDONLY, 0)
if err != nil {
t.Error(err)
} else {
syscall.Close(fd)
}
}
func init() { func init() {
syscall.Umask(0) syscall.Umask(0)
} }
// Copyright 2020 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_test
import (
"context"
"fmt"
"log"
"sync"
"syscall"
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
// WindowsNode emulates Windows FS semantics, which forbids deleting open files.
type WindowsNode struct {
// WindowsNode inherits most functionality from LoopbackNode.
fs.LoopbackNode
mu sync.Mutex
openCount int
}
var _ = (fs.NodeOpener)((*WindowsNode)(nil))
func (n *WindowsNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
fh, flags, errno := n.LoopbackNode.Open(ctx, flags)
if errno == 0 {
n.mu.Lock()
defer n.mu.Unlock()
n.openCount++
}
return fh, flags, errno
}
var _ = (fs.NodeCreater)((*WindowsNode)(nil))
func (n *WindowsNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
inode, fh, flags, errno := n.LoopbackNode.Create(ctx, name, flags, mode, out)
if errno == 0 {
wn := inode.Operations().(*WindowsNode)
wn.openCount++
}
return inode, fh, flags, errno
}
var _ = (fs.NodeReleaser)((*WindowsNode)(nil))
// Release decreases the open count. The kernel doesn't wait with
// returning from close(), so if the caller is too quick to
// unlink/rename after calling close(), this may still trigger EBUSY.
func (n *WindowsNode) Release(ctx context.Context, f fs.FileHandle) syscall.Errno {
n.mu.Lock()
defer n.mu.Unlock()
n.openCount--
if fr, ok := f.(fs.FileReleaser); ok {
return fr.Release(ctx)
}
return 0
}
func isBusy(parent *fs.Inode, name string) bool {
if ch := parent.GetChild(name); ch != nil {
if wn, ok := ch.Operations().(*WindowsNode); ok {
wn.mu.Lock()
defer wn.mu.Unlock()
if wn.openCount > 0 {
return true
}
}
}
return false
}
var _ = (fs.NodeUnlinker)((*WindowsNode)(nil))
func (n *WindowsNode) Unlink(ctx context.Context, name string) syscall.Errno {
if isBusy(n.EmbeddedInode(), name) {
return syscall.EBUSY
}
return n.LoopbackNode.Unlink(ctx, name)
}
func newWindowsNode(rootData *fs.LoopbackRoot, parent *fs.Inode, name string, st *syscall.Stat_t) fs.InodeEmbedder {
n := &WindowsNode{
LoopbackNode: fs.LoopbackNode{
RootData: rootData,
},
}
return n
}
// ExampleLoopbackReuse shows how to build a file system on top of the
// loopback file system.
func Example_loopbackReuse() {
mntDir := "/tmp/mnt"
origDir := "/tmp/orig"
rootData := &fs.LoopbackRoot{
NewNode: newWindowsNode,
Path: origDir,
}
sec := time.Second
opts := &fs.Options{
AttrTimeout: &sec,
EntryTimeout: &sec,
}
server, err := fs.Mount(mntDir, newWindowsNode(rootData, nil, "", nil), opts)
if err != nil {
log.Fatalf("Mount fail: %v\n", err)
}
fmt.Printf("files under %s cannot be deleted if they are opened", mntDir)
server.Wait()
}
// Copyright 2020 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_test
import (
"bytes"
"io/ioutil"
"os"
"syscall"
"testing"
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
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)
rootData := &fs.LoopbackRoot{
NewNode: newWindowsNode,
Path: origDir,
}
opts := fs.Options{}
opts.Debug = testutil.VerboseTest()
server, err := fs.Mount(mntDir, newWindowsNode(rootData, nil, "", nil), &opts)
if err != nil {
t.Fatal(err)
}
defer server.Unmount()
data := []byte("hello")
nm := mntDir + "/file"
if err := ioutil.WriteFile(nm, data, 0644); err != nil {
t.Fatal(err)
}
if got, err := ioutil.ReadFile(nm); err != nil {
t.Fatal(err)
} else if bytes.Compare(got, data) != 0 {
t.Fatalf("got %q want %q", got, data)
}
f, err := os.Open(nm)
if err != nil {
t.Fatal(err)
}
if err := syscall.Unlink(nm); err == nil {
t.Fatal("Unlink should have failed")
}
f.Close()
// Ugh - it may take a while for the RELEASE to be processed.
time.Sleep(10 * time.Millisecond)
if err := syscall.Unlink(nm); err != nil {
t.Fatalf("Unlink: %v", err)
}
}
...@@ -67,21 +67,59 @@ ...@@ -67,21 +67,59 @@
// Typically, each call of the API happens in its own // Typically, each call of the API happens in its own
// goroutine, so take care to make the file system thread-safe. // goroutine, so take care to make the file system thread-safe.
// //
// Be careful when you access the FUSE mount from the same process. An access can
// tie up two OS threads (one on the request side and one on the FUSE server side).
// This can deadlock if there is no free thread to handle the FUSE server side.
// Run your program with GOMAXPROCS=1 to make the problem easier to reproduce,
// 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 // As said above this packages provides way to implement filesystems in terms of
// raw FUSE protocol. Additionally packages nodefs and pathfs provide ways to // raw FUSE protocol.
// implement filesystem at higher levels:
// //
// Package github.com/hanwen/go-fuse/fuse/nodefs provides way to implement // Package github.com/hanwen/go-fuse/v2/fs provides way to implement
// filesystems in terms of inodes. This resembles kernel's idea of what a // filesystems in terms of paths and/or inodes.
// filesystem looks like.
// //
// Package github.com/hanwen/go-fuse/fuse/pathfs provides way to implement // Mount styles
// filesystems in terms of path names. Working with path names is somewhat //
// easier compared to inodes, however renames can be racy. Do not use pathfs if // The NewServer() handles mounting the filesystem, which
// you care about correctness. // involves opening `/dev/fuse` and calling the
// `mount(2)` syscall. The latter needs root permissions.
// This is handled in one of three ways:
//
// 1) go-fuse opens `/dev/fuse` and executes the `fusermount`
// setuid-root helper to call `mount(2)` for us. This is the default.
// Does not need root permissions but needs `fusermount` installed.
//
// 2) If `MountOptions.DirectMount` is set, go-fuse calls `mount(2)` itself.
// Needs root permissions, but works without `fusermount`.
//
// 3) If `mountPoint` has the magic `/dev/fd/N` syntax, it means that that a
// privileged parent process:
//
// * Opened /dev/fuse
//
// * Called mount(2) on a real mountpoint directory that we don't know about
//
// * Inherited the fd to /dev/fuse to us
//
// * Informs us about the fd number via /dev/fd/N
//
// This magic syntax originates from libfuse [1] and allows the FUSE server to
// run without any privileges and without needing `fusermount`, as the parent
// process performs all privileged operations.
//
// The "privileged parent" is usually a container manager like Singularity [2],
// but for testing, it can also be the `mount.fuse3` helper with the
// `drop_privileges,setuid=$USER` flags. Example below for gocryptfs:
//
// $ sudo mount.fuse3 "/usr/local/bin/gocryptfs#/tmp/cipher" /tmp/mnt -o drop_privileges,setuid=$USER
//
// [1] https://github.com/libfuse/libfuse/commit/64e11073b9347fcf9c6d1eea143763ba9e946f70
//
// [2] https://sylabs.io/guides/3.7/user-guide/bind_paths_and_mounts.html#fuse-mounts
package fuse package fuse
// Types for users to implement. // Types for users to implement.
...@@ -159,6 +197,23 @@ type MountOptions struct { ...@@ -159,6 +197,23 @@ type MountOptions struct {
// The filesystem is fully responsible for invalidating data cache. // The filesystem is fully responsible for invalidating data cache.
ExplicitDataCacheControl bool ExplicitDataCacheControl bool
// SyncRead is off by default, which means that go-fuse enable the
// FUSE_CAP_ASYNC_READ capability.
// The kernel then submits multiple concurrent reads to service
// userspace requests and kernel readahead.
//
// Setting SyncRead disables the FUSE_CAP_ASYNC_READ capability.
// The kernel then only sends one read request per file handle at a time,
// and orders the requests by offset.
//
// This is useful if reading out of order or concurrently is expensive for
// (example: Amazon Cloud Drive).
//
// See the comment to FUSE_CAP_ASYNC_READ in
// https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h
// for more details.
SyncRead bool
// If set, fuse will first attempt to use syscall.Mount instead of // If set, fuse will first attempt to use syscall.Mount instead of
// fusermount to mount the filesystem. This will not update /etc/mtab // fusermount to mount the filesystem. This will not update /etc/mtab
// but might be needed if fusermount is not available. // but might be needed if fusermount is not available.
...@@ -167,6 +222,18 @@ type MountOptions struct { ...@@ -167,6 +222,18 @@ type MountOptions struct {
// Options passed to syscall.Mount, the default value used by fusermount // Options passed to syscall.Mount, the default value used by fusermount
// is syscall.MS_NOSUID|syscall.MS_NODEV // is syscall.MS_NOSUID|syscall.MS_NODEV
DirectMountFlags uintptr DirectMountFlags uintptr
// EnableAcls enables kernel ACL support.
//
// See the comments to FUSE_CAP_POSIX_ACL
// in https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h
// for details.
EnableAcl bool
// Disable ReadDirPlus capability so ReadDir is used instead. Simple
// directory queries (i.e. 'ls' without '-l') can be faster with
// ReadDir, as no per-file stat calls are needed
DisableReadDirPlus bool
} }
// RawFileSystem is an interface close to the FUSE wire protocol. // RawFileSystem is an interface close to the FUSE wire protocol.
......
...@@ -23,4 +23,5 @@ func (a *Attr) FromStat(s *syscall.Stat_t) { ...@@ -23,4 +23,5 @@ func (a *Attr) FromStat(s *syscall.Stat_t) {
a.Uid = uint32(s.Uid) a.Uid = uint32(s.Uid)
a.Gid = uint32(s.Gid) a.Gid = uint32(s.Gid)
a.Rdev = uint32(s.Rdev) a.Rdev = uint32(s.Rdev)
a.Blksize = uint32(s.Blksize)
} }
...@@ -18,6 +18,9 @@ const ( ...@@ -18,6 +18,9 @@ const (
FUSE_LK_FLOCK = (1 << 0) FUSE_LK_FLOCK = (1 << 0)
FUSE_RELEASE_FLUSH = (1 << 0)
FUSE_RELEASE_FLOCK_UNLOCK = (1 << 1)
FUSE_IOCTL_MAX_IOV = 256 FUSE_IOCTL_MAX_IOV = 256
FUSE_POLL_SCHEDULE_NOTIFY = (1 << 0) FUSE_POLL_SCHEDULE_NOTIFY = (1 << 0)
......
...@@ -9,89 +9,125 @@ import ( ...@@ -9,89 +9,125 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"unsafe"
) )
func openFUSEDevice() (*os.File, error) { func unixgramSocketpair() (l, r *os.File, err error) {
fs, err := filepath.Glob("/dev/osxfuse*") fd, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err != nil { if err != nil {
return nil, err return nil, nil, os.NewSyscallError("socketpair",
} err.(syscall.Errno))
if len(fs) == 0 {
bin := oldLoadBin
if _, err := os.Stat(newLoadBin); err == nil {
bin = newLoadBin
}
cmd := exec.Command(bin)
if err := cmd.Run(); err != nil {
return nil, err
}
fs, err = filepath.Glob("/dev/osxfuse*")
if err != nil {
return nil, err
}
} }
l = os.NewFile(uintptr(fd[0]), "socketpair-half1")
r = os.NewFile(uintptr(fd[1]), "socketpair-half2")
return
}
for _, fn := range fs { // Create a FUSE FS on the specified mount point. The returned
f, err := os.OpenFile(fn, os.O_RDWR, 0) // mount point is always absolute.
func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, err error) {
local, remote, err := unixgramSocketpair()
if err != nil { if err != nil {
continue return
} }
return f, nil
}
return nil, fmt.Errorf("all FUSE devices busy")
}
const oldLoadBin = "/Library/Filesystems/osxfusefs.fs/Support/load_osxfusefs"
const newLoadBin = "/Library/Filesystems/osxfuse.fs/Contents/Resources/load_osxfuse"
const oldMountBin = "/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs" defer local.Close()
const newMountBin = "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse" defer remote.Close()
func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, err error) { bin, err := fusermountBinary()
f, err := openFUSEDevice()
if err != nil { if err != nil {
return 0, err return 0, err
} }
bin := oldMountBin cmd := exec.Command(bin,
if _, err := os.Stat(newMountBin); err == nil { "-o", strings.Join(opts.optionsStrings(), ","),
bin = newMountBin "-o", fmt.Sprintf("iosize=%d", opts.MaxWrite),
} mountPoint)
cmd.ExtraFiles = []*os.File{remote} // fd would be (index + 3)
cmd := exec.Command(bin, "-o", strings.Join(opts.optionsStrings(), ","), "-o", fmt.Sprintf("iosize=%d", opts.MaxWrite), "3", mountPoint) cmd.Env = append(os.Environ(),
cmd.ExtraFiles = []*os.File{f} "_FUSE_CALL_BY_LIB=",
cmd.Env = append(os.Environ(), "MOUNT_FUSEFS_CALL_BY_LIB=", "MOUNT_OSXFUSE_CALL_BY_LIB=", "_FUSE_DAEMON_PATH="+os.Args[0],
"MOUNT_OSXFUSE_DAEMON_PATH="+os.Args[0], "_FUSE_COMMFD=3",
"MOUNT_FUSEFS_DAEMON_PATH="+os.Args[0]) "_FUSE_COMMVERS=2",
"MOUNT_OSXFUSE_CALL_BY_LIB=",
"MOUNT_OSXFUSE_DAEMON_PATH="+os.Args[0])
var out, errOut bytes.Buffer var out, errOut bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
cmd.Stderr = &errOut cmd.Stderr = &errOut
if err := cmd.Start(); err != nil { if err = cmd.Start(); err != nil {
f.Close() return
return 0, err
} }
go func() {
err := cmd.Wait() fd, err = getConnection(local)
if err != nil { if err != nil {
err = fmt.Errorf("mount_osxfusefs failed: %v. Stderr: %s, Stdout: %s", err, errOut.String(), out.String()) return -1, err
}
go func() {
// wait inside a goroutine or otherwise it would block forever for unknown reasons
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("mount_osxfusefs failed: %v. Stderr: %s, Stdout: %s",
err, errOut.String(), out.String())
} }
ready <- err ready <- err
close(ready) close(ready)
}() }()
// The finalizer for f will close its fd so we return a dup. // golang sets CLOEXEC on file descriptors when they are
defer f.Close() // acquired through normal operations (e.g. open).
return syscall.Dup(int(f.Fd())) // Buf for fd, we have to set CLOEXEC manually
syscall.CloseOnExec(fd)
return fd, err
} }
func unmount(dir string, opts *MountOptions) error { func unmount(dir string, opts *MountOptions) error {
return syscall.Unmount(dir, 0) return syscall.Unmount(dir, 0)
} }
func getConnection(local *os.File) (int, error) {
var data [4]byte
control := make([]byte, 4*256)
// n, oobn, recvflags, from, errno - todo: error checking.
_, oobn, _, _,
err := syscall.Recvmsg(
int(local.Fd()), data[:], control[:], 0)
if err != nil {
return 0, err
}
message := *(*syscall.Cmsghdr)(unsafe.Pointer(&control[0]))
fd := *(*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(&control[0])) + syscall.SizeofCmsghdr))
if message.Type != syscall.SCM_RIGHTS {
return 0, fmt.Errorf("getConnection: recvmsg returned wrong control type: %d", message.Type)
}
if oobn <= syscall.SizeofCmsghdr {
return 0, fmt.Errorf("getConnection: too short control message. Length: %d", oobn)
}
if fd < 0 {
return 0, fmt.Errorf("getConnection: fd < 0: %d", fd)
}
return int(fd), nil
}
func fusermountBinary() (string, error) {
binPaths := []string{
"/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse",
"/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse",
}
for _, path := range binPaths {
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", fmt.Errorf("no FUSE mount utility found")
}
...@@ -68,18 +68,13 @@ func mountDirect(mountPoint string, opts *MountOptions, ready chan<- error) (fd ...@@ -68,18 +68,13 @@ func mountDirect(mountPoint string, opts *MountOptions, ready chan<- error) (fd
return return
} }
// Create a FUSE FS on the specified mount point. The returned // callFusermount calls the `fusermount` suid helper with the right options so
// mount point is always absolute. // that it:
func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, err error) { // * opens `/dev/fuse`
if opts.DirectMount { // * mount()s this file descriptor to `mountPoint`
fd, err := mountDirect(mountPoint, opts, ready) // * passes this file descriptor back to us via a unix domain socket
if err == nil { // This file descriptor is returned as `fd`.
return fd, nil func callFusermount(mountPoint string, opts *MountOptions) (fd int, err error) {
} else if opts.Debug {
log.Printf("mount: failed to do direct mount: %s", err)
}
}
local, remote, err := unixgramSocketpair() local, remote, err := unixgramSocketpair()
if err != nil { if err != nil {
return return
...@@ -97,6 +92,9 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e ...@@ -97,6 +92,9 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e
if s := opts.optionsStrings(); len(s) > 0 { if s := opts.optionsStrings(); len(s) > 0 {
cmd = append(cmd, "-o", strings.Join(s, ",")) cmd = append(cmd, "-o", strings.Join(s, ","))
} }
if opts.Debug {
log.Printf("callFusermount: executing %q", cmd)
}
proc, err := os.StartProcess(bin, proc, err := os.StartProcess(bin,
cmd, cmd,
&os.ProcAttr{ &os.ProcAttr{
...@@ -121,11 +119,39 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e ...@@ -121,11 +119,39 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e
return -1, err return -1, err
} }
return
}
// 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 {
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)
}
}
// Magic `/dev/fd/N` mountpoint. See the docs for NewServer() for how this
// works.
fd = parseFuseFd(mountPoint)
if fd >= 0 {
if opts.Debug {
log.Printf("mount: magic mountpoint %q, using fd %d", mountPoint, fd)
}
} else {
// Usual case: mount via the `fusermount` suid helper
fd, err = callFusermount(mountPoint, opts)
if err != nil {
return
}
}
// golang sets CLOEXEC on file descriptors when they are // golang sets CLOEXEC on file descriptors when they are
// acquired through normal operations (e.g. open). // acquired through normal operations (e.g. open).
// Buf for fd, we have to set CLOEXEC manually // Buf for fd, we have to set CLOEXEC manually
syscall.CloseOnExec(fd) syscall.CloseOnExec(fd)
close(ready) close(ready)
return fd, err return fd, err
} }
...@@ -146,6 +172,9 @@ func unmount(mountPoint string, opts *MountOptions) (err error) { ...@@ -146,6 +172,9 @@ func unmount(mountPoint string, opts *MountOptions) (err error) {
errBuf := bytes.Buffer{} errBuf := bytes.Buffer{}
cmd := exec.Command(bin, "-u", mountPoint) cmd := exec.Command(bin, "-u", mountPoint)
cmd.Stderr = &errBuf cmd.Stderr = &errBuf
if opts.Debug {
log.Printf("unmount: executing %q", cmd.Args)
}
err = cmd.Run() err = cmd.Run()
if errBuf.Len() > 0 { if errBuf.Len() > 0 {
return fmt.Errorf("%s (code %v)\n", return fmt.Errorf("%s (code %v)\n",
...@@ -193,7 +222,12 @@ func lookPathFallback(file string, fallbackDir string) (string, error) { ...@@ -193,7 +222,12 @@ func lookPathFallback(file string, fallbackDir string) (string, error) {
return exec.LookPath(abs) return exec.LookPath(abs)
} }
// fusermountBinary returns the path to the `fusermount3` binary, or, if not
// found, the `fusermount` binary.
func fusermountBinary() (string, error) { func fusermountBinary() (string, error) {
if path, err := lookPathFallback("fusermount3", "/bin"); err == nil {
return path, nil
}
return lookPathFallback("fusermount", "/bin") return lookPathFallback("fusermount", "/bin")
} }
......
package fuse
import (
"fmt"
"io/ioutil"
"syscall"
"testing"
)
// TestMountDevFd tests the special `/dev/fd/N` mountpoint syntax, where a
// privileged parent process opens /dev/fuse and calls mount() for us.
//
// In this test, we simulate a privileged parent by using the `fusermount` suid
// helper.
func TestMountDevFd(t *testing.T) {
realMountPoint, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatal(err)
}
defer syscall.Rmdir(realMountPoint)
// Call the fusermount suid helper to obtain the file descriptor in place
// of a privileged parent.
var fuOpts MountOptions
fd, err := callFusermount(realMountPoint, &fuOpts)
if err != nil {
t.Fatal(err)
}
fdMountPoint := fmt.Sprintf("/dev/fd/%d", fd)
// Real test starts here:
// See if we can feed fdMountPoint to NewServer
fs := NewDefaultRawFileSystem()
opts := MountOptions{
Debug: true,
}
srv, err := NewServer(fs, fdMountPoint, &opts)
if err != nil {
t.Fatal(err)
}
go srv.Serve()
if err := srv.WaitMount(); err != nil {
t.Fatal(err)
}
// If we are actually mounted, we should get ENOSYS.
//
// This won't deadlock despite pollHack not working for `/dev/fd/N` mounts
// because functions in the syscall package don't use the poller.
var st syscall.Stat_t
err = syscall.Stat(realMountPoint, &st)
if err != syscall.ENOSYS {
t.Errorf("expected ENOSYS, got %v", err)
}
// Cleanup is somewhat tricky because `srv` does not know about
// `realMountPoint`, so `srv.Unmount()` cannot work.
//
// A normal user has to call `fusermount -u` for themselves to unmount.
// But in this test we can monkey-patch `srv.mountPoint`.
srv.mountPoint = realMountPoint
if err := srv.Unmount(); err != nil {
t.Error(err)
}
}
// TestMountMaxWrite makes sure that mounting works with all MaxWrite settings.
// We used to fail with EINVAL below 8k because readPool got too small.
func TestMountMaxWrite(t *testing.T) {
opts := []MountOptions{
{MaxWrite: 0}, // go-fuse default
{MaxWrite: 1},
{MaxWrite: 123},
{MaxWrite: 1 * 1024},
{MaxWrite: 4 * 1024},
{MaxWrite: 8 * 1024},
{MaxWrite: 64 * 1024}, // go-fuse default
{MaxWrite: 128 * 1024}, // limit in Linux v4.19 and older
{MaxWrite: 999 * 1024},
{MaxWrite: 1024 * 1024}, // limit in Linux v4.20+
}
for _, o := range opts {
name := fmt.Sprintf("MaxWrite%d", o.MaxWrite)
t.Run(name, func(t *testing.T) {
mnt, err := ioutil.TempDir("", name)
if err != nil {
t.Fatal(err)
}
fs := NewDefaultRawFileSystem()
srv, err := NewServer(fs, mnt, &o)
if err != nil {
t.Error(err)
} else {
go srv.Serve()
srv.Unmount()
}
})
}
}
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"log" "log"
"reflect" "reflect"
"runtime" "runtime"
"syscall"
"time" "time"
"unsafe" "unsafe"
) )
...@@ -100,6 +101,18 @@ func doInit(server *Server, req *request) { ...@@ -100,6 +101,18 @@ func doInit(server *Server, req *request) {
server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS
} }
if server.opts.EnableAcl {
server.kernelSettings.Flags |= CAP_POSIX_ACL
}
if server.opts.SyncRead {
// Clear CAP_ASYNC_READ
server.kernelSettings.Flags &= ^uint32(CAP_ASYNC_READ)
}
if server.opts.DisableReadDirPlus {
// Clear CAP_READDIRPLUS
server.kernelSettings.Flags &= ^uint32(CAP_READDIRPLUS)
}
dataCacheMode := input.Flags & CAP_AUTO_INVAL_DATA dataCacheMode := input.Flags & CAP_AUTO_INVAL_DATA
if server.opts.ExplicitDataCacheControl { if server.opts.ExplicitDataCacheControl {
// we don't want CAP_AUTO_INVAL_DATA even if we cannot go into fully explicit mode // we don't want CAP_AUTO_INVAL_DATA even if we cannot go into fully explicit mode
...@@ -319,7 +332,7 @@ func doBatchForget(server *Server, req *request) { ...@@ -319,7 +332,7 @@ func doBatchForget(server *Server, req *request) {
forgets := *(*[]_ForgetOne)(unsafe.Pointer(h)) forgets := *(*[]_ForgetOne)(unsafe.Pointer(h))
for i, f := range forgets { for i, f := range forgets {
if server.opts.Debug { if server.opts.Debug {
log.Printf("doBatchForget: rx %d %d/%d: FORGET i%d {Nlookup=%d}", log.Printf("doBatchForget: rx %d %d/%d: FORGET n%d {Nlookup=%d}",
req.inHeader.Unique, i+1, len(forgets), f.NodeId, f.Nlookup) req.inHeader.Unique, i+1, len(forgets), f.NodeId, f.Nlookup)
} }
if f.NodeId == pollHackInode { if f.NodeId == pollHackInode {
...@@ -439,7 +452,7 @@ func doStatFs(server *Server, req *request) { ...@@ -439,7 +452,7 @@ func doStatFs(server *Server, req *request) {
} }
func doIoctl(server *Server, req *request) { func doIoctl(server *Server, req *request) {
req.status = ENOSYS req.status = Status(syscall.ENOTTY)
} }
func doDestroy(server *Server, req *request) { func doDestroy(server *Server, req *request) {
...@@ -732,6 +745,7 @@ func init() { ...@@ -732,6 +745,7 @@ func init() {
_OP_SETATTR: func(ptr unsafe.Pointer) interface{} { return (*AttrOut)(ptr) }, _OP_SETATTR: func(ptr unsafe.Pointer) interface{} { return (*AttrOut)(ptr) },
_OP_INIT: func(ptr unsafe.Pointer) interface{} { return (*InitOut)(ptr) }, _OP_INIT: func(ptr unsafe.Pointer) interface{} { return (*InitOut)(ptr) },
_OP_MKDIR: func(ptr unsafe.Pointer) interface{} { return (*EntryOut)(ptr) }, _OP_MKDIR: func(ptr unsafe.Pointer) interface{} { return (*EntryOut)(ptr) },
_OP_MKNOD: func(ptr unsafe.Pointer) interface{} { return (*EntryOut)(ptr) },
_OP_NOTIFY_INVAL_ENTRY: func(ptr unsafe.Pointer) interface{} { return (*NotifyInvalEntryOut)(ptr) }, _OP_NOTIFY_INVAL_ENTRY: func(ptr unsafe.Pointer) interface{} { return (*NotifyInvalEntryOut)(ptr) },
_OP_NOTIFY_INVAL_INODE: func(ptr unsafe.Pointer) interface{} { return (*NotifyInvalInodeOut)(ptr) }, _OP_NOTIFY_INVAL_INODE: func(ptr unsafe.Pointer) interface{} { return (*NotifyInvalInodeOut)(ptr) },
_OP_NOTIFY_STORE_CACHE: func(ptr unsafe.Pointer) interface{} { return (*NotifyStoreOut)(ptr) }, _OP_NOTIFY_STORE_CACHE: func(ptr unsafe.Pointer) interface{} { return (*NotifyStoreOut)(ptr) },
......
...@@ -28,15 +28,25 @@ func doPollHackLookup(ms *Server, req *request) { ...@@ -28,15 +28,25 @@ func doPollHackLookup(ms *Server, req *request) {
Fh: pollHackInode, Fh: pollHackInode,
} }
req.status = OK req.status = OK
case _OP_GETATTR: case _OP_GETATTR, _OP_SETATTR:
out := (*AttrOut)(req.outData()) out := (*AttrOut)(req.outData())
out.Attr = attr out.Attr = attr
req.status = OK req.status = OK
case _OP_GETXATTR:
// Kernel will try to read acl xattrs. Pretend we don't have any.
req.status = ENODATA
case _OP_POLL: case _OP_POLL:
req.status = ENOSYS req.status = ENOSYS
case _OP_ACCESS, _OP_FLUSH, _OP_RELEASE:
// Avoid upsetting the OSX mount process.
req.status = OK
default: default:
// We want to avoid switching off features through our // We want to avoid switching off features through our
// poll hack, so don't use ENOSYS // poll hack, so don't use ENOSYS. It would be nice if
// we could transmit no error code at all, but for
// some opcodes, we'd have to invent credible data to
// return as well.
req.status = ERANGE req.status = ERANGE
} }
} }
...@@ -32,7 +32,7 @@ func pollHack(mountPoint string) error { ...@@ -32,7 +32,7 @@ func pollHack(mountPoint string) error {
POLLHUP = 0x10 POLLHUP = 0x10
) )
fd, err := syscall.Open(filepath.Join(mountPoint, pollHackName), syscall.O_CREAT|syscall.O_TRUNC|syscall.O_RDWR, 0644) fd, err := syscall.Open(filepath.Join(mountPoint, pollHackName), syscall.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -166,17 +166,17 @@ func (in *OpenOut) string() string { ...@@ -166,17 +166,17 @@ func (in *OpenOut) string() string {
} }
func (in *InitIn) string() string { func (in *InitIn) string() string {
return fmt.Sprintf("{%d.%d Ra 0x%x %s}", return fmt.Sprintf("{%d.%d Ra %d %s}",
in.Major, in.Minor, in.MaxReadAhead, in.Major, in.Minor, in.MaxReadAhead,
flagString(initFlagNames, int64(in.Flags), "")) flagString(initFlagNames, int64(in.Flags), ""))
} }
func (o *InitOut) string() string { func (o *InitOut) string() string {
return fmt.Sprintf("{%d.%d Ra 0x%x %s %d/%d Wr 0x%x Tg 0x%x}", return fmt.Sprintf("{%d.%d Ra %d %s %d/%d Wr %d Tg %d MaxPages %d}",
o.Major, o.Minor, o.MaxReadAhead, o.Major, o.Minor, o.MaxReadAhead,
flagString(initFlagNames, int64(o.Flags), ""), flagString(initFlagNames, int64(o.Flags), ""),
o.CongestionThreshold, o.MaxBackground, o.MaxWrite, o.CongestionThreshold, o.MaxBackground, o.MaxWrite,
o.TimeGran) o.TimeGran, o.MaxPages)
} }
func (s *FsyncIn) string() string { func (s *FsyncIn) string() string {
...@@ -219,13 +219,13 @@ func ft(tsec uint64, tnsec uint32) float64 { ...@@ -219,13 +219,13 @@ func ft(tsec uint64, tnsec uint32) float64 {
// Returned by LOOKUP // Returned by LOOKUP
func (o *EntryOut) string() string { func (o *EntryOut) string() string {
return fmt.Sprintf("{i%d g%d tE=%gs tA=%gs %v}", return fmt.Sprintf("{n%d g%d tE=%gs tA=%gs %v}",
o.NodeId, o.Generation, ft(o.EntryValid, o.EntryValidNsec), o.NodeId, o.Generation, ft(o.EntryValid, o.EntryValidNsec),
ft(o.AttrValid, o.AttrValidNsec), &o.Attr) ft(o.AttrValid, o.AttrValidNsec), &o.Attr)
} }
func (o *CreateOut) string() string { func (o *CreateOut) string() string {
return fmt.Sprintf("{i%d g%d %v %v}", o.NodeId, o.Generation, &o.EntryOut, &o.OpenOut) return fmt.Sprintf("{n%d g%d %v %v}", o.NodeId, o.Generation, &o.EntryOut, &o.OpenOut)
} }
func (o *StatfsOut) string() string { func (o *StatfsOut) string() string {
...@@ -248,11 +248,11 @@ func (o *NotifyInvalDeleteOut) string() string { ...@@ -248,11 +248,11 @@ func (o *NotifyInvalDeleteOut) string() string {
} }
func (o *NotifyStoreOut) string() string { func (o *NotifyStoreOut) string() string {
return fmt.Sprintf("{i%d [%d +%d)}", o.Nodeid, o.Offset, o.Size) return fmt.Sprintf("{n%d [%d +%d)}", o.Nodeid, o.Offset, o.Size)
} }
func (o *NotifyRetrieveOut) string() string { func (o *NotifyRetrieveOut) string() string {
return fmt.Sprintf("{> %d: i%d [%d +%d)}", o.NotifyUnique, o.Nodeid, o.Offset, o.Size) return fmt.Sprintf("{> %d: n%d [%d +%d)}", o.NotifyUnique, o.Nodeid, o.Offset, o.Size)
} }
func (i *NotifyRetrieveIn) string() string { func (i *NotifyRetrieveIn) string() string {
...@@ -265,7 +265,7 @@ func (f *FallocateIn) string() string { ...@@ -265,7 +265,7 @@ func (f *FallocateIn) string() string {
} }
func (f *LinkIn) string() string { func (f *LinkIn) string() string {
return fmt.Sprintf("{Oldnodeid: %d}", f.Oldnodeid) return fmt.Sprintf("{Oldnodeid: n%d}", f.Oldnodeid)
} }
func (o *WriteOut) string() string { func (o *WriteOut) string() string {
...@@ -273,7 +273,7 @@ func (o *WriteOut) string() string { ...@@ -273,7 +273,7 @@ func (o *WriteOut) string() string {
} }
func (i *CopyFileRangeIn) string() string { func (i *CopyFileRangeIn) string() string {
return fmt.Sprintf("{Fh %d [%d +%d) => i%d Fh %d [%d, %d)}", return fmt.Sprintf("{Fh %d [%d +%d) => n%d Fh %d [%d, %d)}",
i.FhIn, i.OffIn, i.Len, i.NodeIdOut, i.FhOut, i.OffOut, i.Len) i.FhIn, i.OffIn, i.Len, i.NodeIdOut, i.FhOut, i.OffOut, i.Len)
} }
......
...@@ -18,13 +18,13 @@ func (a *Attr) string() string { ...@@ -18,13 +18,13 @@ func (a *Attr) string() string {
return fmt.Sprintf( return fmt.Sprintf(
"{M0%o SZ=%d L=%d "+ "{M0%o SZ=%d L=%d "+
"%d:%d "+ "%d:%d "+
"%d %d:%d "+ "B%d*%d i%d:%d "+
"A %f "+ "A %f "+
"M %f "+ "M %f "+
"C %f}", "C %f}",
a.Mode, a.Size, a.Nlink, a.Mode, a.Size, a.Nlink,
a.Uid, a.Gid, a.Uid, a.Gid,
a.Blocks, a.Blocks, a.Blksize,
a.Rdev, a.Ino, ft(a.Atime, a.Atimensec), ft(a.Mtime, a.Mtimensec), a.Rdev, a.Ino, ft(a.Atime, a.Atimensec), ft(a.Mtime, a.Mtimensec),
ft(a.Ctime, a.Ctimensec)) ft(a.Ctime, a.Ctimensec))
} }
......
...@@ -105,7 +105,7 @@ func (r *request) InputDebug() string { ...@@ -105,7 +105,7 @@ func (r *request) InputDebug() string {
names += fmt.Sprintf("%s %db", data, len(r.arg)) names += fmt.Sprintf("%s %db", data, len(r.arg))
} }
return fmt.Sprintf("rx %d: %s i%d %s%s", return fmt.Sprintf("rx %d: %s n%d %s%s",
r.inHeader.Unique, operationName(r.inHeader.Opcode), r.inHeader.NodeId, r.inHeader.Unique, operationName(r.inHeader.Opcode), r.inHeader.NodeId,
val, names) val, names)
} }
......
...@@ -8,6 +8,6 @@ const outputHeaderSize = 200 ...@@ -8,6 +8,6 @@ const outputHeaderSize = 200
const ( const (
_FUSE_KERNEL_VERSION = 7 _FUSE_KERNEL_VERSION = 7
_MINIMUM_MINOR_VERSION = 8 _MINIMUM_MINOR_VERSION = 12
_OUR_MINOR_VERSION = 8 _OUR_MINOR_VERSION = 12
) )
...@@ -9,8 +9,10 @@ import ( ...@@ -9,8 +9,10 @@ import (
"log" "log"
"math" "math"
"os" "os"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
...@@ -21,6 +23,13 @@ import ( ...@@ -21,6 +23,13 @@ import (
const ( const (
// The kernel caps writes at 128k. // The kernel caps writes at 128k.
MAX_KERNEL_WRITE = 128 * 1024 MAX_KERNEL_WRITE = 128 * 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
minMaxReaders = 2
maxMaxReaders = 16
) )
// Server contains the logic for reading from the FUSE device and // Server contains the logic for reading from the FUSE device and
...@@ -40,6 +49,9 @@ type Server struct { ...@@ -40,6 +49,9 @@ type Server struct {
opts *MountOptions opts *MountOptions
// maxReaders is the maximum number of goroutines reading requests
maxReaders int
// Pools for []byte // Pools for []byte
buffers bufferPool buffers bufferPool
...@@ -102,10 +114,20 @@ func (ms *Server) RecordLatencies(l LatencyMap) { ...@@ -102,10 +114,20 @@ func (ms *Server) RecordLatencies(l LatencyMap) {
// Unmount calls fusermount -u on the mount. This has the effect of // Unmount calls fusermount -u on the mount. This has the effect of
// shutting down the filesystem. After the Server is unmounted, it // shutting down the filesystem. After the Server is unmounted, it
// should be discarded. // should be discarded.
//
// 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.
func (ms *Server) Unmount() (err error) { func (ms *Server) Unmount() (err error) {
if ms.mountPoint == "" { if ms.mountPoint == "" {
return nil return nil
} }
if parseFuseFd(ms.mountPoint) >= 0 {
return fmt.Errorf("Cannot unmount magic mountpoint %q. Please use `fusermount -u REALMOUNTPOINT` instead.", ms.mountPoint)
}
delay := time.Duration(0) delay := time.Duration(0)
for try := 0; try < 5; try++ { for try := 0; try < 5; try++ {
err = unmount(ms.mountPoint, ms.opts) err = unmount(ms.mountPoint, ms.opts)
...@@ -128,7 +150,11 @@ func (ms *Server) Unmount() (err error) { ...@@ -128,7 +150,11 @@ func (ms *Server) Unmount() (err error) {
return err return err
} }
// NewServer creates a server and attaches it to the given directory. // NewServer creates a FUSE server and attaches ("mounts") it to the
// `mountPoint` directory.
//
// See the "Mount styles" section in the package documentation if you want to
// know about the inner workings of the mount process. Usually you do not.
func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server, error) { func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server, error) {
if opts == nil { if opts == nil {
opts = &MountOptions{ opts = &MountOptions{
...@@ -161,9 +187,17 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server ...@@ -161,9 +187,17 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
} }
} }
maxReaders := runtime.GOMAXPROCS(0)
if maxReaders < minMaxReaders {
maxReaders = minMaxReaders
} else if maxReaders > maxMaxReaders {
maxReaders = maxMaxReaders
}
ms := &Server{ ms := &Server{
fileSystem: fs, fileSystem: fs,
opts: &o, opts: &o,
maxReaders: maxReaders,
retrieveTab: make(map[uint64]*retrieveCacheRequest), retrieveTab: make(map[uint64]*retrieveCacheRequest),
// OSX has races when multiple routines read from the // OSX has races when multiple routines read from the
// FUSE device: on unmount, sometime some reads do not // FUSE device: on unmount, sometime some reads do not
...@@ -177,8 +211,12 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server ...@@ -177,8 +211,12 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
} }
} }
ms.readPool.New = func() interface{} { ms.readPool.New = func() interface{} {
buf := make([]byte, o.MaxWrite+int(maxInputSize)+logicalBlockSize) targetSize := o.MaxWrite + int(maxInputSize)
buf = alignSlice(buf, unsafe.Sizeof(WriteIn{}), logicalBlockSize, uintptr(o.MaxWrite)+maxInputSize) if targetSize < _FUSE_MIN_READ_BUFFER {
targetSize = _FUSE_MIN_READ_BUFFER
}
buf := make([]byte, targetSize+logicalBlockSize)
buf = alignSlice(buf, unsafe.Sizeof(WriteIn{}), logicalBlockSize, uintptr(targetSize))
return buf return buf
} }
mountPoint = filepath.Clean(mountPoint) mountPoint = filepath.Clean(mountPoint)
...@@ -224,6 +262,13 @@ func (o *MountOptions) optionsStrings() []string { ...@@ -224,6 +262,13 @@ func (o *MountOptions) optionsStrings() []string {
r = append(r, "subtype="+o.Name) r = append(r, "subtype="+o.Name)
} }
// OSXFUSE applies a 60-second timeout for file operations. This
// is inconsistent with how FUSE works on Linux, where operations
// last as long as the daemon is willing to let them run.
if runtime.GOOS == "darwin" {
r = append(r, "daemon_timeout=0")
}
return r return r
} }
...@@ -238,10 +283,6 @@ func (ms *Server) DebugData() string { ...@@ -238,10 +283,6 @@ func (ms *Server) DebugData() string {
return fmt.Sprintf("readers: %d", r) return fmt.Sprintf("readers: %d", r)
} }
// What is a good number? Maybe the number of CPUs?
// XXX -> use fuse fd cloning
const _MAX_READERS = 4 // <-- XXX NOTE
// handleEINTR retries the given function until it doesn't return syscall.EINTR. // handleEINTR retries the given function until it doesn't return syscall.EINTR.
// This is similar to the HANDLE_EINTR() macro from Chromium ( see // This is similar to the HANDLE_EINTR() macro from Chromium ( see
// https://code.google.com/p/chromium/codesearch#chromium/src/base/posix/eintr_wrapper.h // https://code.google.com/p/chromium/codesearch#chromium/src/base/posix/eintr_wrapper.h
...@@ -264,17 +305,17 @@ func handleEINTR(fn func() error) (err error) { ...@@ -264,17 +305,17 @@ func handleEINTR(fn func() error) (err error) {
// Returns a new request, or error. In case exitIdle is given, returns // Returns a new request, or error. In case exitIdle is given, returns
// nil, OK if we have too many readers already. // nil, OK if we have too many readers already.
func (ms *Server) readRequest(exitIdle bool) (req *request, code Status) { func (ms *Server) readRequest(exitIdle bool) (req *request, code Status) {
req = ms.reqPool.Get().(*request)
dest := ms.readPool.Get().([]byte)
ms.reqMu.Lock() ms.reqMu.Lock()
if ms.reqReaders > _MAX_READERS { if ms.reqReaders > ms.maxReaders {
ms.reqMu.Unlock() ms.reqMu.Unlock()
return nil, OK return nil, OK
} }
ms.reqReaders++ ms.reqReaders++
ms.reqMu.Unlock() ms.reqMu.Unlock()
req = ms.reqPool.Get().(*request)
dest := ms.readPool.Get().([]byte)
var n int var n int
err := handleEINTR(func() error { err := handleEINTR(func() error {
var err error var err error
...@@ -475,9 +516,16 @@ func (ms *Server) handleRequest(req *request) Status { ...@@ -475,9 +516,16 @@ func (ms *Server) handleRequest(req *request) Status {
errNo := ms.write(req) errNo := ms.write(req)
if errNo != 0 { if errNo != 0 {
// Unless debugging is enabled, ignore ENOENT for INTERRUPT responses
// which indicates that the referred request is no longer known by the
// kernel. This is a normal if the referred request already has
// completed.
if ms.opts.Debug || !(req.inHeader.Opcode == _OP_INTERRUPT && errNo == ENOENT) {
log.Printf("writer: Write/Writev failed, err: %v. opcode: %v", log.Printf("writer: Write/Writev failed, err: %v. opcode: %v",
errNo, operationName(req.inHeader.Opcode)) errNo, operationName(req.inHeader.Opcode))
} }
}
ms.returnRequest(req) ms.returnRequest(req)
return Status(errNo) return Status(errNo)
} }
...@@ -859,5 +907,24 @@ func (ms *Server) WaitMount() error { ...@@ -859,5 +907,24 @@ func (ms *Server) WaitMount() error {
if err != nil { if err != nil {
return err return err
} }
if parseFuseFd(ms.mountPoint) >= 0 {
// Magic `/dev/fd/N` mountpoint. We don't know the real mountpoint, so
// we cannot run the poll hack.
return nil
}
return pollHack(ms.mountPoint) return pollHack(ms.mountPoint)
} }
// parseFuseFd checks if `mountPoint` is the special form /dev/fd/N (with N >= 0),
// and returns N in this case. Returns -1 otherwise.
func parseFuseFd(mountPoint string) (fd int) {
dir, file := path.Split(mountPoint)
if dir != "/dev/fd/" {
return -1
}
fd, err := strconv.Atoi(file)
if err != nil || fd <= 0 {
return -1
}
return fd
}
...@@ -189,11 +189,19 @@ func TestCacheControl(t *testing.T) { ...@@ -189,11 +189,19 @@ func TestCacheControl(t *testing.T) {
defer func() { defer func() {
xmunmap(fmmap) xmunmap(fmmap)
}() }()
xmlock(fmmap)
// assertMmapRead asserts that file's mmaped memory reads as dataOK. // assertMmapRead asserts that file's mmaped memory reads as dataOK.
assertMmapRead := func(subj, dataOK string) { assertMmapRead := func(subj, dataOK string) {
t.Helper() t.Helper()
// Use the Mlock() syscall to get the mmap'ed range into the kernel
// cache again, triggering FUSE reads as neccessary. A blocked syscall does
// not count towards GOMAXPROCS, so there should be a thread available
// to handle the FUSE reads.
// If we don't Mlock() first, the memory comparison triggers a page
// fault, which blocks the thread, and deadlocks the test reliably at
// GOMAXPROCS=1.
// Fixes https://github.com/hanwen/go-fuse/issues/261 .
xmlock(fmmap)
if string(fmmap) != dataOK { if string(fmmap) != dataOK {
t.Fatalf("%s: file mmap: got %q ; want %q", subj, fmmap, dataOK) t.Fatalf("%s: file mmap: got %q ; want %q", subj, fmmap, dataOK)
} }
......
...@@ -81,8 +81,7 @@ func TestDeleteNotify(t *testing.T) { ...@@ -81,8 +81,7 @@ func TestDeleteNotify(t *testing.T) {
return return
} }
buf := bytes.Buffer{} buf := bytes.Buffer{}
cmd := exec.Command("/usr/bin/tail", "-f", "testfile") cmd := exec.Command("/usr/bin/tail", "-f", mnt+"/testdir/testfile")
cmd.Dir = mnt + "/testdir"
cmd.Stdin = &buf cmd.Stdin = &buf
cmd.Stdout = &bytes.Buffer{} cmd.Stdout = &bytes.Buffer{}
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
package test package test
import ( import (
"flag"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"syscall" "syscall"
"testing" "testing"
"time" "time"
...@@ -14,8 +17,18 @@ import ( ...@@ -14,8 +17,18 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil"
) )
var enableOverlayfsTest bool
var enableOverlayfsTestFlag string = "test.overlayfs"
func init() {
// flag to enable test with overayfs. This would not work on kernel 5.15
// so don't enable on that kernel.
flag.BoolVar(&enableOverlayfsTest, enableOverlayfsTestFlag, false, "enable tests with overlayfs (would fail on kernel 5.15)")
}
func TestTouch(t *testing.T) { func TestTouch(t *testing.T) {
ts := NewTestCase(t) ts := NewTestCase(t)
defer ts.Cleanup() defer ts.Cleanup()
...@@ -118,6 +131,40 @@ func clearStatfs(s *syscall.Statfs_t) { ...@@ -118,6 +131,40 @@ func clearStatfs(s *syscall.Statfs_t) {
s.Flags = 0 s.Flags = 0
} }
// Check that fuse mount can serve as a overlayfs lowerdir.
func TestOverlayfs(t *testing.T) {
if !enableOverlayfsTest {
t.Skipf("this test must be enabled through the flag %q", enableOverlayfsTestFlag)
}
if os.Getuid() != 0 {
t.Skip("this test requires root")
}
tc := NewTestCase(t)
defer tc.Cleanup()
testfile := "test"
content := randomData(125)
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()
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 {
t.Fatalf("failed to mount overlay: %v", err)
}
defer unix.Unmount(tmpMergedDir, 0)
err := os.Chtimes(filepath.Join(tmpMergedDir, "subdir", testfile), time.Unix(42, 0), time.Unix(43, 0))
if err != nil {
t.Fatalf("Chtimes failed: %v", err)
}
}
func TestFallocate(t *testing.T) { func TestFallocate(t *testing.T) {
ts := NewTestCase(t) ts := NewTestCase(t)
defer ts.Cleanup() defer ts.Cleanup()
......
...@@ -98,6 +98,11 @@ func TestNodeParallelLookup(t *testing.T) { ...@@ -98,6 +98,11 @@ 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 {
t.Skip("Kernel serializes dir lookups")
}
// spawn 2 threads to access the files in parallel // spawn 2 threads to access the files in parallel
// this will deadlock if nodefs does not allow simultaneous Lookups to be handled. // this will deadlock if nodefs does not allow simultaneous Lookups to be handled.
// see https://github.com/hanwen/go-fuse/commit/d0fca860 for context. // see https://github.com/hanwen/go-fuse/commit/d0fca860 for context.
......
...@@ -265,6 +265,13 @@ type OpenOut struct { ...@@ -265,6 +265,13 @@ type OpenOut struct {
} }
// To be set in InitIn/InitOut.Flags. // To be set in InitIn/InitOut.Flags.
//
// Keep in sync with either of
// * https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/fuse.h
// * https://github.com/libfuse/libfuse/blob/master/include/fuse_kernel.h
// but NOT with
// * https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h
// This file has CAP_HANDLE_KILLPRIV and CAP_POSIX_ACL reversed!
const ( const (
CAP_ASYNC_READ = (1 << 0) CAP_ASYNC_READ = (1 << 0)
CAP_POSIX_LOCKS = (1 << 1) CAP_POSIX_LOCKS = (1 << 1)
......
...@@ -32,6 +32,8 @@ type Attr struct { ...@@ -32,6 +32,8 @@ type Attr struct {
Owner Owner
Rdev uint32 Rdev uint32
Flags_ uint32 // OS X Flags_ uint32 // OS X
Blksize uint32
Padding uint32
} }
const ( const (
...@@ -67,38 +69,52 @@ const ( ...@@ -67,38 +69,52 @@ const (
type GetAttrIn struct { type GetAttrIn struct {
InHeader InHeader
Flags_ uint32
Dummy uint32
Fh_ uint64
} }
func (g *GetAttrIn) Flags() uint32 { func (g *GetAttrIn) Flags() uint32 {
return 0 return g.Flags_
} }
func (g *GetAttrIn) Fh() uint64 { func (g *GetAttrIn) Fh() uint64 {
return 0 return g.Fh_
} }
// Uses OpenIn struct for create. // Uses OpenIn struct for create.
type CreateIn struct { type CreateIn struct {
InHeader InHeader
Flags uint32 Flags uint32
// Mode for the new file; already takes Umask into account.
Mode uint32 Mode uint32
// Umask used for this create call.
Umask uint32
Padding uint32
} }
type MknodIn struct { type MknodIn struct {
InHeader InHeader
// Mode to use, including the Umask value
Mode uint32 Mode uint32
Rdev uint32 Rdev uint32
Umask uint32
Padding uint32
} }
type ReadIn struct { type ReadIn struct {
InHeader InHeader
Fh uint64 Fh uint64
Offset uint64 Offset uint64
Size uint32 Size uint32
ReadFlags uint32 ReadFlags uint32
LockOwner uint64
Flags uint32
Padding uint32
} }
type WriteIn struct { type WriteIn struct {
...@@ -107,6 +123,9 @@ type WriteIn struct { ...@@ -107,6 +123,9 @@ type WriteIn struct {
Offset uint64 Offset uint64
Size uint32 Size uint32
WriteFlags uint32 WriteFlags uint32
LockOwner uint64
Flags uint32
Padding uint32
} }
type SetXAttrIn struct { type SetXAttrIn struct {
......
module github.com/hanwen/go-fuse/v2 module github.com/hanwen/go-fuse/v2
require ( require (
github.com/hanwen/go-fuse v1.0.0
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
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-20180830151530-49385e6e1522
) )
......
github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
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 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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
...@@ -487,11 +487,16 @@ func ReadDir(t *testing.T, mnt string) { ...@@ -487,11 +487,16 @@ func ReadDir(t *testing.T, mnt string) {
got[e] = true got[e] = true
} }
if len(got) != len(want) { if len(got) != len(want) {
t.Errorf("got %d entries, want %d", len(got), len(want)) t.Errorf("mismatch got %d want %d", len(got), len(want))
} }
for k := range got { for k := range got {
if !want[k] { if !want[k] {
t.Errorf("got unknown name %q", k) t.Errorf("got extra entry %q", k)
}
}
for k := range want {
if !got[k] {
t.Errorf("missing entry %q", k)
} }
} }
} }
......
This diff is collapsed.
// 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 unionfs
import (
"io/ioutil"
"os"
"testing"
"time"
"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"
)
const entryTTL = 100 * time.Millisecond
var testAOpts = AutoUnionFsOptions{
UnionFsOptions: testOpts,
Options: nodefs.Options{
EntryTimeout: entryTTL,
AttrTimeout: entryTTL,
NegativeTimeout: 0,
Debug: testutil.VerboseTest(),
LookupKnownChildren: true,
},
HideReadonly: true,
Version: "version",
}
func init() {
testAOpts.Options.Debug = testutil.VerboseTest()
}
func WriteFile(t *testing.T, name string, contents string) {
err := ioutil.WriteFile(name, []byte(contents), 0644)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
}
func setup(t *testing.T) (workdir string, server *fuse.Server, cleanup func()) {
wd := testutil.TempDir()
err := os.Mkdir(wd+"/mnt", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err = os.Mkdir(wd+"/store", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Mkdir(wd+"/ro", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
WriteFile(t, wd+"/ro/file1", "file1")
WriteFile(t, wd+"/ro/file2", "file2")
fs := NewAutoUnionFs(wd+"/store", testAOpts)
nfs := pathfs.NewPathNodeFs(fs, nil)
state, _, err := nodefs.MountRoot(wd+"/mnt", nfs.Root(), &testAOpts.Options)
if err != nil {
t.Fatalf("MountNodeFileSystem failed: %v", err)
}
go state.Serve()
state.WaitMount()
return wd, state, func() {
state.Unmount()
os.RemoveAll(wd)
}
}
func TestDebug(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
c, err := ioutil.ReadFile(wd + "/mnt/status/debug")
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if len(c) == 0 {
t.Fatal("No debug found.")
}
}
func TestVersion(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
c, err := ioutil.ReadFile(wd + "/mnt/status/gounionfs_version")
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if len(c) == 0 {
t.Fatal("No version found.")
}
}
func TestAutoFsSymlink(t *testing.T) {
wd, server, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/store/backing1", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err = os.Symlink(wd+"/ro", wd+"/store/backing1/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/store/backing1", wd+"/mnt/config/manual1")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
fi, err := os.Lstat(wd + "/mnt/manual1/file1")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
entries, err := ioutil.ReadDir(wd + "/mnt")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
if len(entries) != 3 {
t.Error("readdir mismatch", entries)
}
err = os.Remove(wd + "/mnt/config/manual1")
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
scan := wd + "/mnt/config/" + _SCAN_CONFIG
err = ioutil.WriteFile(scan, []byte("something"), 0644)
if err != nil {
t.Error("error writing:", err)
}
// If FUSE supports invalid inode notifications we expect this node to be gone. Otherwise we'll just make sure that it's not reachable.
if server.KernelSettings().SupportsNotify(fuse.NOTIFY_INVAL_INODE) {
fi, _ = os.Lstat(wd + "/mnt/manual1")
if fi != nil {
t.Error("Should not have file:", fi)
}
} else {
entries, err = ioutil.ReadDir(wd + "/mnt")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
for _, e := range entries {
if e.Name() == "manual1" {
t.Error("Should not have entry: ", e)
}
}
}
_, err = os.Lstat(wd + "/mnt/backing1/file1")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
}
func TestDetectSymlinkedDirectories(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/backing1", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err = os.Symlink(wd+"/ro", wd+"/backing1/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/backing1", wd+"/store/backing1")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
scan := wd + "/mnt/config/" + _SCAN_CONFIG
err = ioutil.WriteFile(scan, []byte("something"), 0644)
if err != nil {
t.Error("error writing:", err)
}
_, err = os.Lstat(wd + "/mnt/backing1")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
}
func TestExplicitScan(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/store/backing1", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Symlink(wd+"/ro", wd+"/store/backing1/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
fi, _ := os.Lstat(wd + "/mnt/backing1")
if fi != nil {
t.Error("Should not have file:", fi)
}
scan := wd + "/mnt/config/" + _SCAN_CONFIG
_, err = os.Lstat(scan)
if err != nil {
t.Error(".scan_config missing:", err)
}
err = ioutil.WriteFile(scan, []byte("something"), 0644)
if err != nil {
t.Error("error writing:", err)
}
_, err = os.Lstat(wd + "/mnt/backing1")
if err != nil {
t.Error("Should have workspace backing1:", err)
}
}
func TestCreationChecks(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/store/foo", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Symlink(wd+"/ro", wd+"/store/foo/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Mkdir(wd+"/store/ws2", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Symlink(wd+"/ro", wd+"/store/ws2/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/store/foo", wd+"/mnt/config/bar")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/store/foo", wd+"/mnt/config/foo")
code := fuse.ToStatus(err)
if code != fuse.EBUSY {
t.Error("Should return EBUSY", err)
}
err = os.Symlink(wd+"/store/ws2", wd+"/mnt/config/config")
code = fuse.ToStatus(err)
if code != fuse.EINVAL {
t.Error("Should return EINVAL", err)
}
}
// 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 unionfs
import (
"fmt"
"log"
"strings"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
const _XATTRSEP = "@XATTR@"
type attrResponse struct {
*fuse.Attr
fuse.Status
}
type xattrResponse struct {
data []byte
fuse.Status
}
type dirResponse struct {
entries []fuse.DirEntry
fuse.Status
}
type linkResponse struct {
linkContent string
fuse.Status
}
// Caches filesystem metadata.
type cachingFileSystem struct {
pathfs.FileSystem
attributes *TimedCache
dirs *TimedCache
links *TimedCache
xattr *TimedCache
}
func readDir(fs pathfs.FileSystem, name string) *dirResponse {
origStream, code := fs.OpenDir(name, nil)
r := &dirResponse{nil, code}
if !code.Ok() {
return r
}
r.entries = origStream
return r
}
func getAttr(fs pathfs.FileSystem, name string) *attrResponse {
a, code := fs.GetAttr(name, nil)
return &attrResponse{
Attr: a,
Status: code,
}
}
func getXAttr(fs pathfs.FileSystem, nameAttr string) *xattrResponse {
ns := strings.SplitN(nameAttr, _XATTRSEP, 2)
a, code := fs.GetXAttr(ns[0], ns[1], nil)
return &xattrResponse{
data: a,
Status: code,
}
}
func readLink(fs pathfs.FileSystem, name string) *linkResponse {
a, code := fs.Readlink(name, nil)
return &linkResponse{
linkContent: a,
Status: code,
}
}
func NewCachingFileSystem(fs pathfs.FileSystem, ttl time.Duration) pathfs.FileSystem {
c := new(cachingFileSystem)
c.FileSystem = fs
c.attributes = NewTimedCache(func(n string) (interface{}, bool) {
a := getAttr(fs, n)
return a, a.Ok()
}, ttl)
c.dirs = NewTimedCache(func(n string) (interface{}, bool) {
d := readDir(fs, n)
return d, d.Ok()
}, ttl)
c.links = NewTimedCache(func(n string) (interface{}, bool) {
l := readLink(fs, n)
return l, l.Ok()
}, ttl)
c.xattr = NewTimedCache(func(n string) (interface{}, bool) {
l := getXAttr(fs, n)
return l, l.Ok()
}, ttl)
return c
}
func (fs *cachingFileSystem) DropCache() {
for _, c := range []*TimedCache{fs.attributes, fs.dirs, fs.links, fs.xattr} {
c.DropAll(nil)
}
}
func (fs *cachingFileSystem) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
if name == _DROP_CACHE {
return &fuse.Attr{
Mode: fuse.S_IFREG | 0777,
}, fuse.OK
}
r := fs.attributes.Get(name).(*attrResponse)
return r.Attr, r.Status
}
func (fs *cachingFileSystem) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
key := name + _XATTRSEP + attr
r := fs.xattr.Get(key).(*xattrResponse)
return r.data, r.Status
}
func (fs *cachingFileSystem) Readlink(name string, context *fuse.Context) (string, fuse.Status) {
r := fs.links.Get(name).(*linkResponse)
return r.linkContent, r.Status
}
func (fs *cachingFileSystem) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) {
r := fs.dirs.Get(name).(*dirResponse)
return r.entries, r.Status
}
func (fs *cachingFileSystem) String() string {
return fmt.Sprintf("cachingFileSystem(%v)", fs.FileSystem)
}
func (fs *cachingFileSystem) Open(name string, flags uint32, context *fuse.Context) (f nodefs.File, status fuse.Status) {
if flags&fuse.O_ANYWRITE != 0 && name == _DROP_CACHE {
log.Println("Dropping cache for", fs)
fs.DropCache()
}
return fs.FileSystem.Open(name, flags, context)
}
// 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 unionfs
import (
"os"
"syscall"
"testing"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
func modeMapEq(m1, m2 map[string]uint32) bool {
if len(m1) != len(m2) {
return false
}
for k, v := range m1 {
val, ok := m2[k]
if !ok || val != v {
return false
}
}
return true
}
func TestCachingFs(t *testing.T) {
wd := testutil.TempDir()
defer os.RemoveAll(wd)
fs := pathfs.NewLoopbackFileSystem(wd)
cfs := NewCachingFileSystem(fs, 0)
os.Mkdir(wd+"/orig", 0755)
fi, code := cfs.GetAttr("orig", nil)
if !code.Ok() {
t.Fatal("GetAttr failure", code)
}
if !fi.IsDir() {
t.Error("unexpected attr", fi)
}
os.Symlink("orig", wd+"/symlink")
val, code := cfs.Readlink("symlink", nil)
if val != "orig" {
t.Error("unexpected readlink", val)
}
if !code.Ok() {
t.Error("code !ok ", code)
}
stream, code := cfs.OpenDir("", nil)
if !code.Ok() {
t.Fatal("Readdir fail", code)
}
results := make(map[string]uint32)
for _, v := range stream {
results[v.Name] = v.Mode &^ 07777
}
expected := map[string]uint32{
"symlink": syscall.S_IFLNK,
"orig": fuse.S_IFDIR,
}
if !modeMapEq(results, expected) {
t.Error("Unexpected readdir result", results, expected)
}
}
// 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 unionfs
import (
"os"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
func NewUnionFsFromRoots(roots []string, opts *UnionFsOptions, roCaching bool) (pathfs.FileSystem, error) {
fses := make([]pathfs.FileSystem, 0)
for i, r := range roots {
var fs pathfs.FileSystem
fi, err := os.Stat(r)
if err != nil {
return nil, err
}
if fi.IsDir() {
fs = pathfs.NewLoopbackFileSystem(r)
}
if fs == nil {
return nil, err
}
if i > 0 && roCaching {
fs = NewCachingFileSystem(fs, 0)
}
fses = append(fses, fs)
}
return NewUnionFs(fses, *opts)
}
// 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 unionfs
import (
"sync"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
// newDirnameMap reads the contents of the given directory. On error,
// returns a nil map. This forces reloads in the dirCache until we
// succeed.
func newDirnameMap(fs pathfs.FileSystem, dir string) map[string]struct{} {
stream, code := fs.OpenDir(dir, nil)
if code == fuse.ENOENT {
// The directory not existing is not an error.
return map[string]struct{}{}
}
if !code.Ok() {
return nil
}
result := make(map[string]struct{})
for _, e := range stream {
if e.Mode&fuse.S_IFREG != 0 {
result[e.Name] = struct{}{}
}
}
return result
}
// dirCache caches names in a directory for some time.
//
// If called when the cache is expired, the filenames are read afresh in
// the background.
type dirCache struct {
dir string
ttl time.Duration
fs pathfs.FileSystem
// Protects data below.
lock sync.RWMutex
// If nil, you may call refresh() to schedule a new one.
names map[string]struct{}
updateRunning bool
}
func (c *dirCache) setMap(newMap map[string]struct{}) {
c.lock.Lock()
defer c.lock.Unlock()
c.names = newMap
c.updateRunning = false
_ = time.AfterFunc(c.ttl,
func() { c.DropCache() })
}
func (c *dirCache) DropCache() {
c.lock.Lock()
defer c.lock.Unlock()
c.names = nil
}
// Try to refresh: if another update is already running, do nothing,
// otherwise, read the directory and set it.
func (c *dirCache) maybeRefresh() {
c.lock.Lock()
defer c.lock.Unlock()
if c.updateRunning {
return
}
c.updateRunning = true
go func() {
newmap := newDirnameMap(c.fs, c.dir)
c.setMap(newmap)
}()
}
func (c *dirCache) RemoveEntry(name string) {
c.lock.Lock()
defer c.lock.Unlock()
if c.names == nil {
go c.maybeRefresh()
return
}
delete(c.names, name)
}
func (c *dirCache) AddEntry(name string) {
c.lock.Lock()
defer c.lock.Unlock()
if c.names == nil {
go c.maybeRefresh()
return
}
c.names[name] = struct{}{}
}
func newDirCache(fs pathfs.FileSystem, dir string, ttl time.Duration) *dirCache {
dc := new(dirCache)
dc.dir = dir
dc.fs = fs
dc.ttl = ttl
return dc
}
func (c *dirCache) HasEntry(name string) (mapPresent bool, found bool) {
c.lock.RLock()
defer c.lock.RUnlock()
if c.names == nil {
go c.maybeRefresh()
return false, false
}
_, ok := c.names[name]
return true, ok
}
This diff is collapsed.
// 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 unionfs
import (
"testing"
"time"
)
func TestTimedCacheUncacheable(t *testing.T) {
fetchCount := 0
fetch := func(n string) (interface{}, bool) {
fetchCount++
i := int(n[0])
return &i, false
}
cache := NewTimedCache(fetch, 0)
v := cache.Get("n").(*int)
w := cache.Get("n").(*int)
if *v != int('n') || *w != *v {
t.Errorf("value mismatch: got %d, %d want %d", *v, *w, int('n'))
}
if fetchCount != 2 {
t.Fatalf("Should have fetched twice: %d", fetchCount)
}
}
func TestTimedCache(t *testing.T) {
fetchCount := 0
fetch := func(n string) (interface{}, bool) {
fetchCount++
i := int(n[0])
return &i, true
}
// This fails with 1e6 on some Opteron CPUs.
ttl := 100 * time.Millisecond
cache := NewTimedCache(fetch, ttl)
v := cache.Get("n").(*int)
if *v != int('n') {
t.Errorf("value mismatch: got %d, want %d", *v, int('n'))
}
if fetchCount != 1 {
t.Errorf("fetch count mismatch: got %d want 1", fetchCount)
}
// The cache update is async.
time.Sleep(time.Duration(ttl / 10))
w := cache.Get("n")
if v != w {
t.Errorf("Huh, inconsistent: 1st = %v != 2nd = %v", v, w)
}
if fetchCount > 1 {
t.Errorf("fetch count fail: %d > 1", fetchCount)
}
time.Sleep(time.Duration(ttl * 2))
cache.Purge()
w = cache.Get("n")
if fetchCount == 1 {
t.Error("Did not fetch again. Purge unsuccessful?")
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// 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 unionfs
import (
"syscall"
)
// Darwin doesn't have support for syscall.Getxattr() so we pull it into its own file and implement it by hand on Darwin.
func Getxattr(path string, attr string, dest []byte) (sz int, err error) {
return syscall.Getxattr(path, attr, dest)
}
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