Compare commits

...

41 Commits

Author SHA1 Message Date
Vitaliy Filippov 8d4d89b65d Remove default os.ModeDevice in ConvertFileMode 2023-08-11 00:19:41 +03:00
Vitaliy Filippov efe41d860d Add READDIRPLUS support 2023-08-03 15:36:17 +03:00
Vitaliy Filippov a307ab844b Move convertChildInodeEntry, convertAttributes, convertExpirationTime to fuseops 2023-08-03 15:36:17 +03:00
Vitaliy Filippov 8312d62874 Use fuse_kernel_linux for Windows builds too 2023-06-09 11:47:15 +03:00
Vitaliy Filippov 2d9f6f635e Move Convert(File|Golang)Mode to fuseops 2023-06-04 18:42:22 +03:00
Vitaliy Filippov 8156bfadb4 Do not use syscall.O_ACCMODE 2023-06-04 18:41:56 +03:00
Vitaliy Filippov caac53a719 Add notification ops and conversions 2023-03-23 19:00:51 +03:00
Vitaliy Filippov 0f0f6e3670 Add PollOp 2023-03-22 18:50:52 +03:00
Vitaliy Filippov 062810a628 Add missing poll and notify request structures 2023-03-22 18:46:11 +03:00
Vitaliy Filippov c02e24f8ce Publish file mode conversion functions 2022-11-02 01:37:38 +03:00
Vitaliy Filippov 76071bba22 Add device number support 2022-11-02 01:36:28 +03:00
Vitaliy Filippov f54cd84222 Allow to use "zero-copy" writes 2022-11-01 20:56:02 +03:00
Eric Gouyer a4cd154343 add support for sticky bit on directories 2022-10-16 10:46:58 +02:00
Eric Gouyer 5e958a41f6 add support for retrieving UID GID PID for each fuseops, ala FUSE-C fuse_get_context() 2022-10-16 10:46:58 +02:00
Eric Gouyer 2681cd5156 add support for UID and GID in SetInodeAttributes 2022-10-16 10:46:58 +02:00
Doug Schaapveld 63437da750
Remove PID zero condition from sample memfs (#131) 2022-09-27 17:14:28 +02:00
Avi c62d7682a6
Add more debugging logs to the mounting process so we get more visibility for customer issues (#130) 2022-09-06 08:54:02 +02:00
Michael Stapelberg 4e67748df3 GitHub Actions: use Go 1.19 2022-09-06 08:43:12 +02:00
Michael Stapelberg 66d6bd9e7b gofmt with Go 1.19 2022-09-06 08:42:56 +02:00
Mei Gui 226fec2ce9
Pass OpenFlags for OpenFileOp followups (#129) 2022-07-26 09:34:00 +02:00
Mei Gui 9cc4ff0bc9
Pass OpenFlags for OpenFileOp (#127) 2022-07-18 13:15:27 +02:00
Vitaliy Filippov 13117049f3
Fallback from BatchForgetOp to a series of ForgetInodeOp if unsupported (#126)
Co-authored-by: Vitaliy Filippov <vitalif@yourcmc.ru>
2022-07-02 11:18:25 +02:00
Ben Linsay 21122235c7
allow passing open /dev/fuse file descriptors (#124)
allows passing open /dev/fuse file descriptors so that the FUSE process
can run fully unprivileged. uses the /dev/fd/N mountpoint format from
libfuse3.
2022-05-31 22:22:54 +02:00
Doychin Atanasov 37d63df227
Add support for the FUSE_BATCH_FORGET operation (#123)
There are certain Kernel versions which do not send individual
Forget operations after they receive "not implemented" for Batch Forget.
One example is "5.4.0-110-generic" on Ubuntu 20.04. I am sure there are
plenty of others.

This leads to inode "leaks". Where reference counts are never decreased
and the inodes were left hanging around long after they are not needed
any more.

The best way to fix that was adding support for batch operations to the
lib. This way all users will be able to benefit from the batching
optimization.

Co-authored-by: Doychin Atanasov <doychin.atanasov@chaosgroup.com>
2022-05-27 08:49:15 +02:00
Michael Stapelberg 468f285a46 update go.mod to pull in latest x/sys and x/net
Hopefully this fixes CI failures on mac
2022-05-27 08:33:30 +02:00
Michael Stapelberg 48612565d5 GitHub Actions: upgrade to latest stable version of Go 1.17 2022-03-03 09:31:36 +01:00
Michael Stapelberg 1c9fe7bc84 remove memclr/memmove entirely now that they are unused
This breaks with newer Go versions AFAICT, so it’s easier to just remove it entirely.
2022-03-03 09:28:15 +01:00
Vitaliy Filippov 84920d11dd Add vectored read to readbenchfs
You can now run `./readbenchfs --mount_point dir --vectored` and then
`dd if=dir/test of=/dev/null iflag=direct bs=1M status=progress` to test
vectored read speed.

Results from my laptop (Linux 5.10):

1 core (GOMAXPROCS=1):
- Before vectored read patch: 2.1 GB/s
- Non-vectored read after vectored read patch: 2.1 GB/s
- Vectored read: 2.8 GB/s

All cores:
- Before vectored read patch: 3.0 GB/s
- Non-vectored read after vectored read patch: 3.3 GB/s
- Vectored read: 5.9 GB/s
2022-03-03 09:25:18 +01:00
Vitaliy Filippov c818f6216b Implement vectored read support
Read requests can now take vectored responses from the filesystem
implementation and send them to FUSE device via the writev() system call.

This allows file systems to send data without copying it into the
library-provided buffer if the data is already in memory.
2022-03-03 09:25:18 +01:00
Vitaliy Filippov da71c70600 Add ReadBenchFS to test linear read speed 2022-03-03 09:25:18 +01:00
Jakob Waibel c4197873da
Create mount_memfs sample (#121) 2022-02-14 20:12:19 +01:00
Vitaliy Filippov 108387eec1
Fix RenameOp compatibility with macfuse 4.x (#107)
Closed-source macfuse 4.x has broken compatibility with osxfuse 3.x:
it passes an additional 64-bit field (flags) after RenameIn regardless
that we don't enable the support for RENAME_SWAP/RENAME_EXCL.

The simplest fix is just to check for the presence of all-zero flags
and strip them when they're present.

Co-authored-by: Vitaliy Filippov <vitalif@yourcmc.ru>
2022-01-30 14:39:55 +01:00
Jakob Waibel 1b9b09fd17
feat: Remove duplicate code in test (#119) 2022-01-09 15:54:07 +01:00
Jakob Waibel ffd6c474e8
docs: add missing “kernel” (#117) 2021-11-25 17:36:55 +01:00
Tetsuo Kiso 7c4418392f
Add gofmt CI step (#115) 2021-11-08 15:02:43 +01:00
Tetsuo Kiso c0eeb00f17
Delete .travis.yml (#114) 2021-11-07 18:19:45 +01:00
Tetsuo Kiso 233e2c82a4
Disable go test in CI (#113) 2021-11-07 18:19:20 +01:00
Tetsuo Kiso a621cd4bb0
Switch from Travis CI to GitHub Actions (#111)
Fixes #99.
2021-11-07 17:30:55 +01:00
Oliver Le Zhuang 8da59ba998
InMessage is initialized with a storage allocated before reads (#110)
Fixes #109. In #102, the storage of the InMessage gets allocated every
time it's being read, which is expensive and caused issues like
https://github.com/GoogleCloudPlatform/gcsfuse/issues/563. So, this
change moves the allocation to its own function, called only once when
the struct is initialized before the reads.
2021-10-28 23:41:51 +02:00
Vitaliy Filippov c75d3f26fc
Use newer mounting method (similar to fusermount) with macfuse 4.x (#106)
macfuse 4.x turns out to be incompatible with the old mounting method where
you open the device by yourself and only supports the newer method where you
receive a file descriptor from `mount_macfuse` through a unix socket.

Co-authored-by: Vitaliy Filippov <vitalif@yourcmc.ru>
2021-10-19 18:50:09 +02:00
mcdhee-msft 95fc8d1181
Add configuration to control async reads (#105) 2021-09-04 17:48:39 +02:00
47 changed files with 1721 additions and 680 deletions

67
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,67 @@
name: ci
on:
push:
branches:
- master
pull_request:
branches:
- '*'
jobs:
gofmt:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2.1.4
with:
go-version: ^1.19
id: go
# Check the codebase has been formatted by `gofmt`.
# We run two `gofmt` commands for different purposes:
# 1. `gofmt -d .`: to print diffs in the CI log if any.
# 2. `gofmt -l`: to test there is a diff in the codebase. This is because
# `gofmt` exits with exit status 0 even if there is a diff.
# This is recommended in https://github.com/golang/go/issues/24230)
- name: Run gofmt
run: |
gofmt -d .
test -z $(gofmt -l .)
linux-tests:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2.1.4
with:
go-version: ^1.19
id: go
- name: Install fuse
run: sudo apt-get update && sudo apt-get install -y fuse3 libfuse-dev
- name: Build
run: go build ./...
# Disabled running `go test` because running tests hung at random,
# preventing us from running the tests in CI reliably.
# (cf. https://github.com/jacobsa/fuse/issues/97)
macos-build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2.1.4
with:
go-version: ^1.19
id: go
- name: Install macfuse
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install macfuse
- name: Build
run: |
go build ./...
go build ./samples/mount_hello/... ./samples/mount_roloopbackfs/... ./samples/mount_sample/...
# Skip running tests as `go test` hung in macOS.

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
*.so
# Folders
.idea/
_obj
_test

View File

@ -1,35 +0,0 @@
# Cf. http://docs.travis-ci.com/user/getting-started/
# Cf. http://docs.travis-ci.com/user/languages/go/
matrix:
include:
- os: linux
language: go
go: 1.16
# Use the virtualized Trusty beta Travis is running in order to get
# support for installing fuse.
#
# Cf. Personal communication from support@travis-ci.com.
dist: trusty
sudo: required
- os: osx
# Pin to macOS 10.12 (indirectly by using the Xcode 8.3 image). We must
# do this to get the OSXFUSE kernel extension to work because there
# currently is no know way to programmatically grant permissions to load
# a kernel extension in macOS 10.13.
#
# See https://github.com/travis-ci/travis-ci/issues/10017 for details.
osx_image: xcode8.3
language: go
go: 1.16
# Install fuse before installing our code.
before_install:
# For linux: install fuse.
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
sudo apt-get install -qq fuse;
fi
# For macOS: update homebrew and then install osxfuse.
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew cask install osxfuse; fi

View File

@ -1,3 +1,4 @@
[![ci](https://github.com/jacobsa/fuse/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/jacobsa/fuse/actions/workflows/ci.yml)
[![GoDoc](https://godoc.org/github.com/jacobsa/ogletest?status.svg)](https://godoc.org/github.com/jacobsa/fuse)
This package allows for writing and mounting user-space file systems from Go.

View File

@ -39,17 +39,17 @@ var contextKey interface{} = contextKeyType(0)
//
// As of 2015-03-26, the behavior in the kernel is:
//
// * (http://goo.gl/bQ1f1i, http://goo.gl/HwBrR6) Set the local variable
// - (http://goo.gl/bQ1f1i, http://goo.gl/HwBrR6) Set the local variable
// ra_pages to be init_response->max_readahead divided by the page size.
//
// * (http://goo.gl/gcIsSh, http://goo.gl/LKV2vA) Set
// - (http://goo.gl/gcIsSh, http://goo.gl/LKV2vA) Set
// backing_dev_info::ra_pages to the min of that value and what was sent
// in the request's max_readahead field.
//
// * (http://goo.gl/u2SqzH) Use backing_dev_info::ra_pages when deciding
// - (http://goo.gl/u2SqzH) Use backing_dev_info::ra_pages when deciding
// how much to read ahead.
//
// * (http://goo.gl/JnhbdL) Don't read ahead at all if that field is zero.
// - (http://goo.gl/JnhbdL) Don't read ahead at all if that field is zero.
//
// Reading a page at a time is a drag. Ask for a larger size.
const maxReadahead = 1 << 20
@ -151,6 +151,7 @@ func (c *Connection) Init() error {
cacheSymlinks := initOp.Flags&fusekernel.InitCacheSymlinks > 0
noOpenSupport := initOp.Flags&fusekernel.InitNoOpenSupport > 0
noOpendirSupport := initOp.Flags&fusekernel.InitNoOpendirSupport > 0
readdirplusSupport := initOp.Flags&fusekernel.InitDoReaddirplus > 0
// Respond to the init op.
initOp.Library = c.protocol
@ -161,6 +162,11 @@ func (c *Connection) Init() error {
// Tell the kernel not to use pitifully small 4 KiB writes.
initOp.Flags |= fusekernel.InitBigWrites
if c.cfg.EnableAsyncReads {
initOp.Flags |= fusekernel.InitAsyncRead
}
// kernel 4.20 increases the max from 32 -> 256
initOp.Flags |= fusekernel.InitMaxPages
initOp.MaxPages = 256
@ -188,6 +194,11 @@ func (c *Connection) Init() error {
initOp.Flags |= fusekernel.InitNoOpendirSupport
}
// Tell the kernel to do readdirplus (readdir+lookup in one call)
if c.cfg.UseReadDirPlus && readdirplusSupport {
initOp.Flags |= fusekernel.InitDoReaddirplus
}
c.Reply(ctx, nil)
return nil
}
@ -333,7 +344,7 @@ func (c *Connection) readMessage() (*buffer.InMessage, error) {
// Loop past transient errors.
for {
// Attempt a reaed.
// Attempt a read.
err := m.Init(c.dev)
// Special cases:
@ -364,18 +375,24 @@ func (c *Connection) readMessage() (*buffer.InMessage, error) {
}
// Write the supplied message to the kernel.
func (c *Connection) writeMessage(msg []byte) error {
// Avoid the retry loop in os.File.Write.
n, err := syscall.Write(int(c.dev.Fd()), msg)
if err != nil {
return err
func (c *Connection) writeMessage(outMsg *buffer.OutMessage) error {
var err error
var n int
expectedLen := outMsg.Len()
if outMsg.Sglist != nil {
n, err = writev(int(c.dev.Fd()), outMsg.Sglist)
} else {
// Avoid the retry loop in os.File.Write.
n, err = syscall.Write(int(c.dev.Fd()), outMsg.OutHeaderBytes())
}
if n != len(msg) {
return fmt.Errorf("Wrote %d bytes; expected %d", n, len(msg))
if err == nil && n != expectedLen {
err = fmt.Errorf("Wrote %d bytes; expected %d", n, expectedLen)
}
return nil
if err != nil && c.errorLogger != nil {
c.errorLogger.Printf("writeMessage: %v %v", err, outMsg.OutHeaderBytes())
}
outMsg.Sglist = nil
return err
}
// ReadOp consumes the next op from the kernel process, returning the op and a
@ -400,7 +417,7 @@ func (c *Connection) ReadOp() (_ context.Context, op interface{}, _ error) {
// Convert the message to an op.
outMsg := c.getOutMessage()
op, err = convertInMessage(inMsg, outMsg, c.protocol)
op, err = convertInMessage(&c.cfg, inMsg, outMsg, c.protocol)
if err != nil {
c.putOutMessage(outMsg)
return nil, nil, fmt.Errorf("convertInMessage: %v", err)
@ -480,8 +497,15 @@ func (c *Connection) Reply(ctx context.Context, opErr error) {
outMsg := state.outMsg
fuseID := inMsg.Header().Unique
suppressReuse := false
if wr, ok := op.(*fuseops.WriteFileOp); ok {
suppressReuse = wr.SuppressReuse
}
// Make sure we destroy the messages when we're done.
defer c.putInMessage(inMsg)
if !suppressReuse {
defer c.putInMessage(inMsg)
}
defer c.putOutMessage(outMsg)
// Clean up state for this op.
@ -505,13 +529,21 @@ func (c *Connection) Reply(ctx context.Context, opErr error) {
noResponse := c.kernelResponse(outMsg, inMsg.Header().Unique, op, opErr)
if !noResponse {
err := c.writeMessage(outMsg.Bytes())
if err != nil && c.errorLogger != nil {
c.errorLogger.Printf("writeMessage: %v %v", err, outMsg.Bytes())
}
c.writeMessage(outMsg)
}
}
// Send a notification to the kernel
// notification must be a pointer to one of fuseops.NotifyXXX structures
// To avoid a deadlock notifications must not be called in the execution path of a related filesytem operation or within any code that could hold a lock that could be needed to execute such an operation. As of kernel 4.18, a "related operation" is a lookup(), symlink(), mknod(), mkdir(), unlink(), rename(), link() or create() request for the parent, and a setattr(), unlink(), rmdir(), rename(), setxattr(), removexattr(), readdir() or readdirplus() request for the inode itself.
func (c *Connection) Notify(notification interface{}) error {
outMsg := c.getOutMessage()
defer c.putOutMessage(outMsg)
c.kernelNotification(outMsg, notification)
outMsg.OutHeader().Len = uint32(outMsg.Len())
return c.writeMessage(outMsg)
}
// Close the connection. Must not be called until operations that were read
// from the connection have been responded to.
func (c *Connection) close() error {

View File

@ -38,6 +38,7 @@ import (
//
// The caller is responsible for arranging for the message to be destroyed.
func convertInMessage(
config *MountConfig,
inMsg *buffer.InMessage,
outMsg *buffer.OutMessage,
protocol fusekernel.Protocol) (o interface{}, err error) {
@ -75,12 +76,20 @@ func convertInMessage(
o = to
valid := fusekernel.SetattrValid(in.Valid)
if valid&fusekernel.SetattrUid != 0 {
to.Uid = &in.Uid
}
if valid&fusekernel.SetattrGid != 0 {
to.Gid = &in.Gid
}
if valid&fusekernel.SetattrSize != 0 {
to.Size = &in.Size
}
if valid&fusekernel.SetattrMode != 0 {
mode := convertFileMode(in.Mode)
mode := fuseops.ConvertFileMode(in.Mode)
to.Mode = &mode
}
@ -112,6 +121,32 @@ func convertInMessage(
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
case fusekernel.OpBatchForget:
type input fusekernel.BatchForgetCountIn
in := (*input)(inMsg.Consume(unsafe.Sizeof(input{})))
if in == nil {
return nil, errors.New("Corrupt OpBatchForget")
}
entries := make([]fuseops.BatchForgetEntry, 0, in.Count)
for i := uint32(0); i < in.Count; i++ {
type entry fusekernel.BatchForgetEntryIn
ein := (*entry)(inMsg.Consume(unsafe.Sizeof(entry{})))
if ein == nil {
return nil, errors.New("Corrupt OpBatchForget")
}
entries = append(entries, fuseops.BatchForgetEntry{
Inode: fuseops.InodeID(ein.Inode),
N: ein.Nlookup,
})
}
o = &fuseops.BatchForgetOp{
Entries: entries,
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
case fusekernel.OpMkdir:
in := (*fusekernel.MkdirIn)(inMsg.Consume(fusekernel.MkdirInSize(protocol)))
if in == nil {
@ -135,7 +170,7 @@ func convertInMessage(
// the fact that this is a directory is implicit in the fact that the
// opcode is mkdir. But we want the correct mode to go through, so ensure
// that os.ModeDir is set.
Mode: convertFileMode(in.Mode) | os.ModeDir,
Mode: fuseops.ConvertFileMode(in.Mode) | os.ModeDir,
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
@ -155,7 +190,8 @@ func convertInMessage(
o = &fuseops.MkNodeOp{
Parent: fuseops.InodeID(inMsg.Header().Nodeid),
Name: string(name),
Mode: convertFileMode(in.Mode),
Mode: fuseops.ConvertFileMode(in.Mode),
Rdev: in.Rdev,
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
@ -175,7 +211,7 @@ func convertInMessage(
o = &fuseops.CreateFileOp{
Parent: fuseops.InodeID(inMsg.Header().Nodeid),
Name: string(name),
Mode: convertFileMode(in.Mode),
Mode: fuseops.ConvertFileMode(in.Mode),
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
@ -206,6 +242,19 @@ func convertInMessage(
}
names := inMsg.ConsumeBytes(inMsg.Len())
// closed-source macfuse 4.x has broken compatibility with osxfuse 3.x:
// it passes an additional 64-bit field (flags) after RenameIn regardless
// that we don't enable the support for RENAME_SWAP/RENAME_EXCL
// macfuse doesn't want change the behaviour back which is motivated by
// not breaking compatibility the second time, look here for details:
// https://github.com/osxfuse/osxfuse/issues/839
//
// the simplest fix is just to check for the presence of all-zero flags
if len(names) >= 8 &&
names[0] == 0 && names[1] == 0 && names[2] == 0 && names[3] == 0 &&
names[4] == 0 && names[5] == 0 && names[6] == 0 && names[7] == 0 {
names = names[8:]
}
// names should be "old\x00new\x00"
if len(names) < 4 {
return nil, errors.New("Corrupt OpRename")
@ -254,8 +303,15 @@ func convertInMessage(
}
case fusekernel.OpOpen:
type input fusekernel.OpenIn
in := (*input)(inMsg.Consume(unsafe.Sizeof(input{})))
if in == nil {
return nil, errors.New("Corrupt OpOpen")
}
o = &fuseops.OpenFileOp{
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
OpenFlags: fusekernel.OpenFlags(in.Flags),
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
@ -275,21 +331,18 @@ func convertInMessage(
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
Handle: fuseops.HandleID(in.Fh),
Offset: int64(in.Offset),
Size: int64(in.Size),
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
if !config.UseVectoredRead {
// Use part of the incoming message storage as the read buffer
// For vectored zero-copy reads, don't allocate any buffers
to.Dst = inMsg.GetFree(int(in.Size))
}
o = to
readSize := int(in.Size)
p := outMsg.GrowNoZero(readSize)
if p == nil {
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
}
sh := (*reflect.SliceHeader)(unsafe.Pointer(&to.Dst))
sh.Data = uintptr(p)
sh.Len = readSize
sh.Cap = readSize
case fusekernel.OpReaddirplus:
fallthrough
case fusekernel.OpReaddir:
in := (*fusekernel.ReadIn)(inMsg.Consume(fusekernel.ReadInSize(protocol)))
if in == nil {
@ -300,12 +353,13 @@ func convertInMessage(
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
Handle: fuseops.HandleID(in.Fh),
Offset: fuseops.DirOffset(in.Offset),
Plus: inMsg.Header().Opcode == fusekernel.OpReaddirplus,
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
o = to
readSize := int(in.Size)
p := outMsg.GrowNoZero(readSize)
p := outMsg.Grow(readSize)
if p == nil {
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
}
@ -476,15 +530,19 @@ func convertInMessage(
o = to
readSize := int(in.Size)
p := outMsg.GrowNoZero(readSize)
if p == nil {
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
}
if readSize > 0 {
p := outMsg.Grow(readSize)
if p == nil {
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
}
sh := (*reflect.SliceHeader)(unsafe.Pointer(&to.Dst))
sh.Data = uintptr(p)
sh.Len = readSize
sh.Cap = readSize
sh := (*reflect.SliceHeader)(unsafe.Pointer(&to.Dst))
sh.Data = uintptr(p)
sh.Len = readSize
sh.Cap = readSize
} else {
to.Dst = nil
}
case fusekernel.OpListxattr:
type input fusekernel.ListxattrIn
@ -501,7 +559,7 @@ func convertInMessage(
readSize := int(in.Size)
if readSize != 0 {
p := outMsg.GrowNoZero(readSize)
p := outMsg.Grow(readSize)
if p == nil {
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
}
@ -552,6 +610,42 @@ func convertInMessage(
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
case fusekernel.OpPoll:
type input fusekernel.PollIn
in := (*input)(inMsg.Consume(unsafe.Sizeof(input{})))
if in == nil {
return nil, errors.New("Corrupt OpPoll")
}
o = &fuseops.PollOp{
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
Handle: fuseops.HandleID(in.Fh),
Kh: in.Kh,
Flags: fusekernel.PollFlags(in.Flags),
Events: fusekernel.PollEvents(in.Events),
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
case fusekernel.OpNotifyReply:
type input fusekernel.NotifyRetrieveIn
in := (*input)(inMsg.Consume(unsafe.Sizeof(input{})))
if in == nil {
return nil, errors.New("Corrupt OpNotifyReply")
}
buf := inMsg.ConsumeBytes(inMsg.Len())
if len(buf) < int(in.Size) {
return nil, errors.New("Corrupt OpNotifyReply")
}
o = &fuseops.NotifyRetrieveReplyOp{
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
Unique: inMsg.Header().Unique,
Offset: in.Offset,
Length: in.Size,
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
}
default:
o = &unknownOp{
OpCode: inMsg.Header().Opcode,
@ -582,6 +676,12 @@ func (c *Connection) kernelResponse(
case *fuseops.ForgetInodeOp:
return true
case *fuseops.BatchForgetOp:
return true
case *fuseops.NotifyRetrieveReplyOp:
return true
case *interruptOp:
return true
}
@ -625,37 +725,37 @@ func (c *Connection) kernelResponseForOp(
case *fuseops.LookUpInodeOp:
size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
fuseops.ConvertChildInodeEntry(&o.Entry, out)
case *fuseops.GetInodeAttributesOp:
size := int(fusekernel.AttrOutSize(c.protocol))
out := (*fusekernel.AttrOut)(m.Grow(size))
out.AttrValid, out.AttrValidNsec = convertExpirationTime(
out.AttrValid, out.AttrValidNsec = fuseops.ConvertExpirationTime(
o.AttributesExpiration)
convertAttributes(o.Inode, &o.Attributes, &out.Attr)
fuseops.ConvertAttributes(o.Inode, &o.Attributes, &out.Attr)
case *fuseops.SetInodeAttributesOp:
size := int(fusekernel.AttrOutSize(c.protocol))
out := (*fusekernel.AttrOut)(m.Grow(size))
out.AttrValid, out.AttrValidNsec = convertExpirationTime(
out.AttrValid, out.AttrValidNsec = fuseops.ConvertExpirationTime(
o.AttributesExpiration)
convertAttributes(o.Inode, &o.Attributes, &out.Attr)
fuseops.ConvertAttributes(o.Inode, &o.Attributes, &out.Attr)
case *fuseops.MkDirOp:
size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
fuseops.ConvertChildInodeEntry(&o.Entry, out)
case *fuseops.MkNodeOp:
size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
fuseops.ConvertChildInodeEntry(&o.Entry, out)
case *fuseops.CreateFileOp:
eSize := int(fusekernel.EntryOutSize(c.protocol))
e := (*fusekernel.EntryOut)(m.Grow(eSize))
convertChildInodeEntry(&o.Entry, e)
fuseops.ConvertChildInodeEntry(&o.Entry, e)
oo := (*fusekernel.OpenOut)(m.Grow(int(unsafe.Sizeof(fusekernel.OpenOut{}))))
oo.Fh = uint64(o.Handle)
@ -663,12 +763,12 @@ func (c *Connection) kernelResponseForOp(
case *fuseops.CreateSymlinkOp:
size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
fuseops.ConvertChildInodeEntry(&o.Entry, out)
case *fuseops.CreateLinkOp:
size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out)
fuseops.ConvertChildInodeEntry(&o.Entry, out)
case *fuseops.RenameOp:
// Empty response
@ -705,9 +805,11 @@ func (c *Connection) kernelResponseForOp(
}
case *fuseops.ReadFileOp:
// convertInMessage already set up the destination buffer to be at the end
// of the out message. We need only shrink to the right size based on how
// much the user read.
if o.Dst != nil {
m.Append(o.Dst)
} else {
m.Append(o.Data...)
}
m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead)
case *fuseops.WriteFileOp:
@ -803,6 +905,13 @@ func (c *Connection) kernelResponseForOp(
out.TimeGran = 1
out.MaxPages = o.MaxPages
case *fuseops.PollOp:
out := (*fusekernel.PollOut)(m.Grow(int(unsafe.Sizeof(fusekernel.PollOut{}))))
out.Revents = uint32(o.Revents)
case *fuseops.NotifyRetrieveReplyOp:
// Empty response
default:
panic(fmt.Sprintf("Unexpected op: %#v", op))
}
@ -810,115 +919,73 @@ func (c *Connection) kernelResponseForOp(
return
}
// Like kernelResponse, but assumes the user replied with a nil error to the op.
func (c *Connection) kernelNotification(
m *buffer.OutMessage,
op interface{}) {
h := m.OutHeader()
h.Unique = 0
// Create the appropriate output message
switch o := op.(type) {
case *fuseops.NotifyPollWakeup:
h.Error = fusekernel.NotifyCodePoll
out := (*fusekernel.NotifyPollWakeupOut)(m.Grow(int(unsafe.Sizeof(fusekernel.NotifyPollWakeupOut{}))))
out.Kh = o.Kh
case *fuseops.NotifyInvalInode:
h.Error = fusekernel.NotifyCodeInvalInode
out := (*fusekernel.NotifyInvalInodeOut)(m.Grow(int(unsafe.Sizeof(fusekernel.NotifyInvalInodeOut{}))))
out.Ino = uint64(o.Inode)
out.Off = o.Offset
out.Len = o.Length
case *fuseops.NotifyInvalEntry:
h.Error = fusekernel.NotifyCodeInvalEntry
out := (*fusekernel.NotifyInvalEntryOut)(m.Grow(int(unsafe.Sizeof(fusekernel.NotifyInvalEntryOut{}))))
out.Parent = uint64(o.Parent)
out.Namelen = uint32(len(o.Name))
m.AppendString(o.Name)
m.AppendString("\x00")
case *fuseops.NotifyDelete:
h.Error = fusekernel.NotifyCodeDelete
out := (*fusekernel.NotifyDeleteOut)(m.Grow(int(unsafe.Sizeof(fusekernel.NotifyDeleteOut{}))))
out.Parent = uint64(o.Parent)
out.Child = uint64(o.Child)
out.Namelen = uint32(len(o.Name))
m.AppendString(o.Name)
m.AppendString("\x00")
case *fuseops.NotifyStore:
h.Error = fusekernel.NotifyCodeStore
out := (*fusekernel.NotifyStoreOut)(m.Grow(int(unsafe.Sizeof(fusekernel.NotifyStoreOut{}))))
out.Nodeid = uint64(o.Inode)
out.Offset = o.Offset
out.Size = o.Length
m.Append(o.Data...)
m.ShrinkTo(buffer.OutMessageHeaderSize + int(unsafe.Sizeof(fusekernel.NotifyStoreOut{})) + int(o.Length))
case *fuseops.NotifyRetrieve:
h.Error = fusekernel.NotifyCodeRetrieve
out := (*fusekernel.NotifyRetrieveOut)(m.Grow(int(unsafe.Sizeof(fusekernel.NotifyRetrieveOut{}))))
out.Unique = o.Unique
out.Nodeid = uint64(o.Inode)
out.Offset = o.Offset
out.Size = o.Length
default:
panic(fmt.Sprintf("Unexpected notification: %#v", op))
}
return
}
////////////////////////////////////////////////////////////////////////
// General conversions
////////////////////////////////////////////////////////////////////////
func convertTime(t time.Time) (secs uint64, nsec uint32) {
totalNano := t.UnixNano()
secs = uint64(totalNano / 1e9)
nsec = uint32(totalNano % 1e9)
return secs, nsec
}
func convertAttributes(
inodeID fuseops.InodeID,
in *fuseops.InodeAttributes,
out *fusekernel.Attr) {
out.Ino = uint64(inodeID)
out.Size = in.Size
out.Atime, out.AtimeNsec = convertTime(in.Atime)
out.Mtime, out.MtimeNsec = convertTime(in.Mtime)
out.Ctime, out.CtimeNsec = convertTime(in.Ctime)
out.SetCrtime(convertTime(in.Crtime))
out.Nlink = in.Nlink
out.Uid = in.Uid
out.Gid = in.Gid
// round up to the nearest 512 boundary
out.Blocks = (in.Size + 512 - 1) / 512
// Set the mode.
out.Mode = uint32(in.Mode) & 0777
switch {
default:
out.Mode |= syscall.S_IFREG
case in.Mode&os.ModeDir != 0:
out.Mode |= syscall.S_IFDIR
case in.Mode&os.ModeDevice != 0:
if in.Mode&os.ModeCharDevice != 0 {
out.Mode |= syscall.S_IFCHR
} else {
out.Mode |= syscall.S_IFBLK
}
case in.Mode&os.ModeNamedPipe != 0:
out.Mode |= syscall.S_IFIFO
case in.Mode&os.ModeSymlink != 0:
out.Mode |= syscall.S_IFLNK
case in.Mode&os.ModeSocket != 0:
out.Mode |= syscall.S_IFSOCK
}
if in.Mode&os.ModeSetuid != 0 {
out.Mode |= syscall.S_ISUID
}
}
// Convert an absolute cache expiration time to a relative time from now for
// consumption by the fuse kernel module.
func convertExpirationTime(t time.Time) (secs uint64, nsecs uint32) {
// Fuse represents durations as unsigned 64-bit counts of seconds and 32-bit
// counts of nanoseconds (cf. http://goo.gl/EJupJV). So negative durations
// are right out. There is no need to cap the positive magnitude, because
// 2^64 seconds is well longer than the 2^63 ns range of time.Duration.
d := t.Sub(time.Now())
if d > 0 {
secs = uint64(d / time.Second)
nsecs = uint32((d % time.Second) / time.Nanosecond)
}
return secs, nsecs
}
func convertChildInodeEntry(
in *fuseops.ChildInodeEntry,
out *fusekernel.EntryOut) {
out.Nodeid = uint64(in.Child)
out.Generation = uint64(in.Generation)
out.EntryValid, out.EntryValidNsec = convertExpirationTime(in.EntryExpiration)
out.AttrValid, out.AttrValidNsec = convertExpirationTime(in.AttributesExpiration)
convertAttributes(in.Child, &in.Attributes, &out.Attr)
}
func convertFileMode(unixMode uint32) os.FileMode {
mode := os.FileMode(unixMode & 0777)
switch unixMode & syscall.S_IFMT {
case syscall.S_IFREG:
// nothing
case syscall.S_IFDIR:
mode |= os.ModeDir
case syscall.S_IFCHR:
mode |= os.ModeCharDevice | os.ModeDevice
case syscall.S_IFBLK:
mode |= os.ModeDevice
case syscall.S_IFIFO:
mode |= os.ModeNamedPipe
case syscall.S_IFLNK:
mode |= os.ModeSymlink
case syscall.S_IFSOCK:
mode |= os.ModeSocket
default:
// no idea
mode |= os.ModeDevice
}
if unixMode&syscall.S_ISUID != 0 {
mode |= os.ModeSetuid
}
if unixMode&syscall.S_ISGID != 0 {
mode |= os.ModeSetgid
}
return mode
}
func writeXattrSize(m *buffer.OutMessage, size uint32) {
out := (*fusekernel.GetxattrOut)(m.Grow(int(unsafe.Sizeof(fusekernel.GetxattrOut{}))))
out.Size = size

View File

@ -95,7 +95,7 @@ func describeRequest(op interface{}) (s string) {
case *fuseops.ReadFileOp:
addComponent("handle %d", typed.Handle)
addComponent("offset %d", typed.Offset)
addComponent("%d bytes", len(typed.Dst))
addComponent("%d bytes", typed.Size)
case *fuseops.WriteFileOp:
addComponent("handle %d", typed.Handle)

8
doc.go
View File

@ -16,15 +16,15 @@
//
// The primary elements of interest are:
//
// * The fuseops package, which defines the operations that fuse might send
// - The fuseops package, which defines the operations that fuse might send
// to your userspace daemon.
//
// * The Server interface, which your daemon must implement.
// - The Server interface, which your daemon must implement.
//
// * fuseutil.NewFileSystemServer, which offers a convenient way to implement
// - fuseutil.NewFileSystemServer, which offers a convenient way to implement
// the Server interface.
//
// * Mount, a function that allows for mounting a Server as a file system.
// - Mount, a function that allows for mounting a Server as a file system.
//
// Make sure to see the examples in the sub-packages of samples/, which double
// as tests for this package: http://godoc.org/github.com/jacobsa/fuse/samples

View File

@ -31,7 +31,7 @@ func (c *Connection) getInMessage() *buffer.InMessage {
c.mu.Unlock()
if x == nil {
x = new(buffer.InMessage)
x = buffer.NewInMessage()
}
return x

84
fuseops/conv.go Normal file
View File

@ -0,0 +1,84 @@
// Copyright 2023 Vitaliy Filippov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fuseops
import (
"time"
"syscall"
"github.com/jacobsa/fuse/internal/fusekernel"
)
////////////////////////////////////////////////////////////////////////
// General conversions
////////////////////////////////////////////////////////////////////////
func ConvertTime(t time.Time) (secs uint64, nsec uint32) {
totalNano := t.UnixNano()
secs = uint64(totalNano / 1e9)
nsec = uint32(totalNano % 1e9)
return secs, nsec
}
func ConvertAttributes(
inodeID InodeID,
in *InodeAttributes,
out *fusekernel.Attr) {
out.Ino = uint64(inodeID)
out.Size = in.Size
out.Atime, out.AtimeNsec = ConvertTime(in.Atime)
out.Mtime, out.MtimeNsec = ConvertTime(in.Mtime)
out.Ctime, out.CtimeNsec = ConvertTime(in.Ctime)
out.SetCrtime(ConvertTime(in.Crtime))
out.Nlink = in.Nlink
out.Uid = in.Uid
out.Gid = in.Gid
// round up to the nearest 512 boundary
out.Blocks = (in.Size + 512 - 1) / 512
// Set the mode.
out.Mode = ConvertGolangMode(in.Mode)
if out.Mode & (syscall.S_IFCHR | syscall.S_IFBLK) != 0 {
out.Rdev = in.Rdev
}
}
// Convert an absolute cache expiration time to a relative time from now for
// consumption by the fuse kernel module.
func ConvertExpirationTime(t time.Time) (secs uint64, nsecs uint32) {
// Fuse represents durations as unsigned 64-bit counts of seconds and 32-bit
// counts of nanoseconds (cf. http://goo.gl/EJupJV). So negative durations
// are right out. There is no need to cap the positive magnitude, because
// 2^64 seconds is well longer than the 2^63 ns range of time.Duration.
d := t.Sub(time.Now())
if d > 0 {
secs = uint64(d / time.Second)
nsecs = uint32((d % time.Second) / time.Nanosecond)
}
return secs, nsecs
}
func ConvertChildInodeEntry(
in *ChildInodeEntry,
out *fusekernel.EntryOut) {
out.Nodeid = uint64(in.Child)
out.Generation = uint64(in.Generation)
out.EntryValid, out.EntryValidNsec = ConvertExpirationTime(in.EntryExpiration)
out.AttrValid, out.AttrValidNsec = ConvertExpirationTime(in.AttributesExpiration)
ConvertAttributes(in.Child, &in.Attributes, &out.Attr)
}

70
fuseops/filemode.go Normal file
View File

@ -0,0 +1,70 @@
package fuseops
import (
"os"
"syscall"
)
func ConvertFileMode(unixMode uint32) os.FileMode {
mode := os.FileMode(unixMode & 0777)
switch unixMode & syscall.S_IFMT {
case syscall.S_IFREG:
// nothing
case syscall.S_IFDIR:
mode |= os.ModeDir
case syscall.S_IFCHR:
mode |= os.ModeCharDevice | os.ModeDevice
case syscall.S_IFBLK:
mode |= os.ModeDevice
case syscall.S_IFIFO:
mode |= os.ModeNamedPipe
case syscall.S_IFLNK:
mode |= os.ModeSymlink
case syscall.S_IFSOCK:
mode |= os.ModeSocket
default:
// no idea
}
if unixMode&syscall.S_ISUID != 0 {
mode |= os.ModeSetuid
}
if unixMode&syscall.S_ISGID != 0 {
mode |= os.ModeSetgid
}
if unixMode&syscall.S_ISVTX != 0 {
mode |= os.ModeSticky
}
return mode
}
func ConvertGolangMode(inMode os.FileMode) uint32 {
outMode := uint32(inMode) & 0777
switch {
default:
outMode |= syscall.S_IFREG
case inMode&os.ModeDir != 0:
outMode |= syscall.S_IFDIR
case inMode&os.ModeDevice != 0:
if inMode&os.ModeCharDevice != 0 {
outMode |= syscall.S_IFCHR
} else {
outMode |= syscall.S_IFBLK
}
case inMode&os.ModeNamedPipe != 0:
outMode |= syscall.S_IFIFO
case inMode&os.ModeSymlink != 0:
outMode |= syscall.S_IFLNK
case inMode&os.ModeSocket != 0:
outMode |= syscall.S_IFSOCK
}
if inMode&os.ModeSetuid != 0 {
outMode |= syscall.S_ISUID
}
if inMode&os.ModeSetgid != 0 {
outMode |= syscall.S_ISGID
}
if inMode&os.ModeSticky != 0 {
outMode |= syscall.S_ISVTX
}
return outMode
}

View File

@ -17,6 +17,8 @@ package fuseops
import (
"os"
"time"
"github.com/jacobsa/fuse/internal/fusekernel"
)
////////////////////////////////////////////////////////////////////////
@ -35,16 +37,16 @@ type OpContext struct {
//
// Called by statfs(2) and friends:
//
// * (https://goo.gl/Xi1lDr) sys_statfs called user_statfs, which calls
// vfs_statfs, which calls statfs_by_dentry.
// - (https://goo.gl/Xi1lDr) sys_statfs called user_statfs, which calls
// vfs_statfs, which calls statfs_by_dentry.
//
// * (https://goo.gl/VAIOwU) statfs_by_dentry calls the superblock
// operation statfs, which in our case points at
// fuse_statfs (cf. https://goo.gl/L7BTM3)
// - (https://goo.gl/VAIOwU) statfs_by_dentry calls the superblock
// operation statfs, which in our case points at
// fuse_statfs (cf. https://goo.gl/L7BTM3)
//
// * (https://goo.gl/Zn7Sgl) fuse_statfs sends a statfs op, then uses
// convert_fuse_statfs to convert the response in a straightforward
// manner.
// - (https://goo.gl/Zn7Sgl) fuse_statfs sends a statfs op, then uses
// convert_fuse_statfs to convert the response in a straightforward
// manner.
//
// This op is particularly important on OS X: if you don't implement it, the
// file system will not successfully mount. If you don't model a sane amount of
@ -159,6 +161,8 @@ type SetInodeAttributesOp struct {
Handle *HandleID
// The attributes to modify, or nil for attributes that don't need a change.
Uid *uint32
Gid *uint32
Size *uint64
Mode *os.FileMode
Atime *time.Time
@ -189,10 +193,10 @@ type SetInodeAttributesOp struct {
// The reference count corresponds to fuse_inode::nlookup
// (http://goo.gl/ut48S4). Some examples of where the kernel manipulates it:
//
// * (http://goo.gl/vPD9Oh) Any caller to fuse_iget increases the count.
// * (http://goo.gl/B6tTTC) fuse_lookup_name calls fuse_iget.
// * (http://goo.gl/IlcxWv) fuse_create_open calls fuse_iget.
// * (http://goo.gl/VQMQul) fuse_dentry_revalidate increments after
// - (http://goo.gl/vPD9Oh) Any caller to fuse_iget increases the count.
// - (http://goo.gl/B6tTTC) fuse_lookup_name calls fuse_iget.
// - (http://goo.gl/IlcxWv) fuse_create_open calls fuse_iget.
// - (http://goo.gl/VQMQul) fuse_dentry_revalidate increments after
// revalidating.
//
// In contrast to all other inodes, RootInodeID begins with an implicit
@ -200,12 +204,12 @@ type SetInodeAttributesOp struct {
// could be no such op, because the root cannot be referred to by name.) Code
// walk:
//
// * (http://goo.gl/gWAheU) fuse_fill_super calls fuse_get_root_inode.
// - (http://goo.gl/gWAheU) fuse_fill_super calls fuse_get_root_inode.
//
// * (http://goo.gl/AoLsbb) fuse_get_root_inode calls fuse_iget without
// - (http://goo.gl/AoLsbb) fuse_get_root_inode calls fuse_iget without
// sending any particular request.
//
// * (http://goo.gl/vPD9Oh) fuse_iget increments nlookup.
// - (http://goo.gl/vPD9Oh) fuse_iget increments nlookup.
//
// File systems should tolerate but not rely on receiving forget ops for
// remaining inodes when the file system unmounts, including the root inode.
@ -220,6 +224,32 @@ type ForgetInodeOp struct {
OpContext OpContext
}
// BatchForgetEntry represents one Inode entry to forget in the BatchForgetOp.
//
// Everything written in the ForgetInodeOp docs applies for the BatchForgetEntry
// too.
type BatchForgetEntry struct {
// The inode whose reference count should be decremented.
Inode InodeID
// The amount to decrement the reference count.
N uint64
}
// Decrement the reference counts for a list of inode IDs previously issued by the file
// system.
//
// This operation is a batch of ForgetInodeOp operations. Every entry in
// Entries is one ForgetInodeOp operation. See the docs of ForgetInodeOp
// for further details.
type BatchForgetOp struct {
// Entries is a list of Forget operations. One could treat every entry in the
// list as a single ForgetInodeOp operation.
Entries []BatchForgetEntry
OpContext OpContext
}
////////////////////////////////////////////////////////////////////////
// Inode creation
////////////////////////////////////////////////////////////////////////
@ -271,6 +301,9 @@ type MkNodeOp struct {
Name string
Mode os.FileMode
// The device number (only valid if created file is a device)
Rdev uint32
// Set by the file system: information about the inode that was created.
//
// The lookup count for the inode is implicitly incremented. See notes on
@ -311,7 +344,7 @@ type CreateFileOp struct {
// The handle may be supplied in future ops like ReadFileOp that contain a
// file handle. The file system must ensure this ID remains valid until a
// later call to ReleaseFileHandle.
Handle HandleID
Handle HandleID
OpContext OpContext
}
@ -372,19 +405,19 @@ type CreateLinkOp struct {
// file system boundaries, and that the destination doesn't already exist with
// the wrong type. Some subtleties that the file system must care about:
//
// * If the new name is an existing directory, the file system must ensure it
// - If the new name is an existing directory, the file system must ensure it
// is empty before replacing it, returning ENOTEMPTY otherwise. (This is
// per the posix spec: http://goo.gl/4XtT79)
//
// * The rename must be atomic from the point of view of an observer of the
// - The rename must be atomic from the point of view of an observer of the
// new name. That is, if the new name already exists, there must be no
// point at which it doesn't exist.
//
// * It is okay for the new name to be modified before the old name is
// - It is okay for the new name to be modified before the old name is
// removed; these need not be atomic. In fact, the Linux man page
// explicitly says this is likely (cf. https://goo.gl/Y1wVZc).
//
// * Linux bends over backwards (https://goo.gl/pLDn3r) to ensure that
// - Linux bends over backwards (https://goo.gl/pLDn3r) to ensure that
// neither the old nor the new parent can be concurrently modified. But
// it's not clear whether OS X does this, and in any case it doesn't matter
// for file systems that may be modified remotely. Therefore a careful file
@ -394,7 +427,6 @@ type CreateLinkOp struct {
// posix and the man pages are imprecise about the actual semantics of a
// rename if it's not atomic, so it is probably not disastrous to be loose
// about this.
//
type RenameOp struct {
// The old parent directory, and the name of the entry within it to be
// relocated.
@ -443,7 +475,7 @@ type UnlinkOp struct {
// Open a directory inode.
//
// On Linux the sends this when setting up a struct file for a particular inode
// On Linux the kernel sends this when setting up a struct file for a particular inode
// with type directory, usually in response to an open(2) call from a
// user-space process. On OS X it may not be sent for every open(2) (cf.
// https://github.com/osxfuse/osxfuse/issues/199).
@ -530,12 +562,18 @@ type ReadDirOp struct {
// offset, and return array offsets into that cached listing.
Offset DirOffset
// Whether this operation is a READDIRPLUS
//
// If true, then the FS must return inode attributes and expiration time
// along with each directory entry and increment its reference count.
Plus bool
// The destination buffer, whose length gives the size of the read.
//
// The output data should consist of a sequence of FUSE directory entries in
// the format generated by fuse_add_direntry (http://goo.gl/qCcHCV), which is
// consumed by parse_dirfile (http://goo.gl/2WUmD2). Use fuseutil.WriteDirent
// to generate this data.
// or fuseutil.WriteDirentPlus to generate this data.
//
// Each entry returned exposes a directory offset to the user that may later
// show up in ReadDirRequest.Offset. See notes on that field for more
@ -578,7 +616,7 @@ type ReleaseDirHandleOp struct {
// Open a file inode.
//
// On Linux the sends this when setting up a struct file for a particular inode
// On Linux the kernel sends this when setting up a struct file for a particular inode
// with type file, usually in response to an open(2) call from a user-space
// process. On OS X it may not be sent for every open(2)
// (cf.https://github.com/osxfuse/osxfuse/issues/199).
@ -620,6 +658,8 @@ type OpenFileOp struct {
// advance, for example, because contents are generated on the fly.
UseDirectIO bool
OpenFlags fusekernel.OpenFlags
OpContext OpContext
}
@ -637,9 +677,17 @@ type ReadFileOp struct {
// The offset within the file at which to read.
Offset int64
// The size of the read.
Size int64
// The destination buffer, whose length gives the size of the read.
// For vectored reads, this field is always nil as the buffer is not provided.
Dst []byte
// Set by the file system:
// A list of slices of data to send back to the client for vectored reads.
Data [][]byte
// Set by the file system: the number of bytes read.
//
// The FUSE documentation requires that exactly the requested number of bytes
@ -660,13 +708,13 @@ type ReadFileOp struct {
// cache and the page is marked dirty. Later the kernel may write back the
// page via the FUSE VFS layer, causing this op to be sent:
//
// * The kernel calls address_space_operations::writepage when a dirty page
// - The kernel calls address_space_operations::writepage when a dirty page
// needs to be written to backing store (cf. http://goo.gl/Ezbewg). Fuse
// sets this to fuse_writepage (cf. http://goo.gl/IeNvLT).
//
// * (http://goo.gl/Eestuy) fuse_writepage calls fuse_writepage_locked.
// - (http://goo.gl/Eestuy) fuse_writepage calls fuse_writepage_locked.
//
// * (http://goo.gl/RqYIxY) fuse_writepage_locked makes a write request to
// - (http://goo.gl/RqYIxY) fuse_writepage_locked makes a write request to
// the userspace server.
//
// Note that the kernel *will* ensure that writes are received and acknowledged
@ -710,7 +758,18 @@ type WriteFileOp struct {
// be written, except on error (http://goo.gl/KUpwwn). This appears to be
// because it uses file mmapping machinery (http://goo.gl/SGxnaN) to write a
// page at a time.
Data []byte
Data []byte
// Set by the file system: "no reuse" flag.
//
// By default, the Data buffer is reused by the library, so the file system
// must copy the data if it wants to use it later.
//
// However, if the file system sets this flag to true, the library doesn't
// reuse this buffer, so the file system can safely store and use Data slice
// without copying memory.
SuppressReuse bool
OpContext OpContext
}
@ -719,10 +778,10 @@ type WriteFileOp struct {
// vfs.txt documents this as being called for by the fsync(2) system call
// (cf. http://goo.gl/j9X8nB). Code walk for that case:
//
// * (http://goo.gl/IQkWZa) sys_fsync calls do_fsync, calls vfs_fsync, calls
// - (http://goo.gl/IQkWZa) sys_fsync calls do_fsync, calls vfs_fsync, calls
// vfs_fsync_range.
//
// * (http://goo.gl/5L2SMy) vfs_fsync_range calls f_op->fsync.
// - (http://goo.gl/5L2SMy) vfs_fsync_range calls f_op->fsync.
//
// Note that this is also sent by fdatasync(2) (cf. http://goo.gl/01R7rF), and
// may be sent for msync(2) with the MS_SYNC flag (see the notes on
@ -743,8 +802,8 @@ type SyncFileOp struct {
// vfs.txt documents this as being sent for each close(2) system call (cf.
// http://goo.gl/FSkbrq). Code walk for that case:
//
// * (http://goo.gl/e3lv0e) sys_close calls __close_fd, calls filp_close.
// * (http://goo.gl/nI8fxD) filp_close calls f_op->flush (fuse_flush).
// - (http://goo.gl/e3lv0e) sys_close calls __close_fd, calls filp_close.
// - (http://goo.gl/nI8fxD) filp_close calls f_op->flush (fuse_flush).
//
// But note that this is also sent in other contexts where a file descriptor is
// closed, such as dup2(2) (cf. http://goo.gl/NQDvFS). In the case of close(2),
@ -753,15 +812,15 @@ type SyncFileOp struct {
// One potentially significant case where this may not be sent is mmap'd files,
// where the behavior is complicated:
//
// * munmap(2) does not cause flushes (cf. http://goo.gl/j8B9g0).
// - munmap(2) does not cause flushes (cf. http://goo.gl/j8B9g0).
//
// * On OS X, if a user modifies a mapped file via the mapping before
// - On OS X, if a user modifies a mapped file via the mapping before
// closing the file with close(2), the WriteFileOps for the modifications
// may not be received before the FlushFileOp for the close(2) (cf.
// https://github.com/osxfuse/osxfuse/issues/202). It appears that this may
// be fixed in osxfuse 3 (cf. https://goo.gl/rtvbko).
//
// * However, you safely can arrange for writes via a mapping to be
// - However, you safely can arrange for writes via a mapping to be
// flushed by calling msync(2) followed by close(2). On OS X msync(2)
// will cause a WriteFileOps to go through and close(2) will cause a
// FlushFile as usual (cf. http://goo.gl/kVmNcx). On Linux, msync(2) does
@ -787,8 +846,8 @@ type SyncFileOp struct {
// return any errors that occur.
type FlushFileOp struct {
// The file and handle being flushed.
Inode InodeID
Handle HandleID
Inode InodeID
Handle HandleID
OpContext OpContext
}
@ -923,3 +982,127 @@ type FallocateOp struct {
Mode uint32
OpContext OpContext
}
// Request notifications when the file system user calls poll/select or
// similar operations on a file.
type PollOp struct {
// The inode and handle the user wants to poll
Inode InodeID
Handle HandleID
// Kh is the "kernel handle". The reason behind it is that it's allocated
// by the kernel on file allocation and guaranteed to be unique as opposed
// to regular file handles (HandleID) generated by the userland server
// (by us). Kh has to be used in NotifyPollWakeupOut replies.
Kh uint64
// Poll flags
Flags fusekernel.PollFlags
// Requested events
Events fusekernel.PollEvents
// Set by the file system: the actual events that have happened
// since the last poll
Revents fusekernel.PollEvents
OpContext OpContext
}
// Notify consumers waiting for poll/epoll that events are incoming
// for the specified kernel handle. The kernel will send a PollOp request
// to get the event mask after receiving this notification
type NotifyPollWakeup struct {
Kh uint64
}
// Notify to invalidate cache for an inode.
//
// If the filesystem has writeback caching enabled, invalidating an inode
// will first trigger a writeback of all dirty pages. The call will block
// until all writeback requests have completed and the inode has been
// invalidated. It will, however, not wait for completion of pending writeback
// requests that have been issued before.
type NotifyInvalInode struct {
Inode InodeID
Offset int64
Length int64
}
// Notify to invalidate parent attributes and the dentry matching parent/name
//
// To avoid a deadlock this request must not be sent in the execution path
// of a related filesytem operation or within any code that could hold a lock
// that could be needed to execute such an operation. As of kernel 4.18, a
// "related operation" is a lookup(), symlink(), mknod(), mkdir(), unlink(),
// rename(), link() or create() request for the parent, and a setattr(),
// unlink(), rmdir(), rename(), setxattr(), removexattr(), readdir() or
// readdirplus() request for the inode itself.
//
// When called correctly, it will never block.
type NotifyInvalEntry struct {
Parent InodeID
Name string
}
// This request behaves like NotifyInvalEntry with the following additional
// effect (at least as of Linux kernel 4.8):
//
// If the provided child inode matches the inode that is currently associated
// with the cached dentry, and if there are any inotify watches registered for
// the dentry, then the watchers are informed that the dentry has been deleted.
//
// To avoid a deadlock this request must not be sent while executing a
// related filesytem operation or while holding a lock that could be needed to
// execute such an operation.
type NotifyDelete struct {
Parent InodeID
Child InodeID
Name string
}
// Store data to the kernel buffers
//
// Synchronously store data in the kernel buffers belonging to the given inode.
// The stored data is marked up-to-date (no read will be performed against it,
// unless it's invalidated or evicted from the cache).
//
// If the stored data overflows the current file size, then the size is extended,
// similarly to a write(2) on the filesystem.
//
// If this request returns an error, then the store wasn't fully completed, but
// it may have been partially completed.
type NotifyStore struct {
Inode InodeID
Offset uint64
Length uint32
Data [][]byte
}
// Retrieve data from the kernel buffers belonging to the given inode
//
// If successful then the kernel will send a NotifyRetrieveReplyOp as a reply.
// Only present pages are returned in the retrieve reply. Retrieving stops when it
// finds a non-present page and only data prior to that is returned.
//
// If this request returns an error, then the retrieve will not be completed and
// no reply will be sent.
//
// This request doesn't change the dirty state of pages in the kernel buffer. For
// dirty pages the write() method will be called regardless of having been retrieved
// previously.
type NotifyRetrieve struct {
Inode InodeID
Unique uint64
Offset uint64
Length uint32
}
// Matches the size of WriteIn
type NotifyRetrieveReplyOp struct {
Inode InodeID
Unique uint64
Offset uint64
Length uint32
OpContext OpContext
}

View File

@ -87,6 +87,9 @@ type InodeAttributes struct {
//
Mode os.FileMode
// The device number. Only valid if the file is a device
Rdev uint32
// Time information. See `man 2 stat` for full details.
Atime time.Time // Time of last access
Mtime time.Time // Time of last modification
@ -118,12 +121,11 @@ func (a *InodeAttributes) DebugString() string {
//
// Some related reading:
//
// http://fuse.sourceforge.net/doxygen/structfuse__entry__param.html
// http://stackoverflow.com/q/11071996/1505451
// http://goo.gl/CqvwyX
// http://julipedia.meroh.net/2005/09/nfs-file-handles.html
// http://goo.gl/wvo3MB
//
// http://fuse.sourceforge.net/doxygen/structfuse__entry__param.html
// http://stackoverflow.com/q/11071996/1505451
// http://goo.gl/CqvwyX
// http://julipedia.meroh.net/2005/09/nfs-file-handles.html
// http://goo.gl/wvo3MB
type GenerationNumber uint64
// HandleID is an opaque 64-bit number used to identify a particular open

View File

@ -19,6 +19,7 @@ import (
"unsafe"
"github.com/jacobsa/fuse/fuseops"
"github.com/jacobsa/fuse/internal/fusekernel"
)
type DirentType uint32
@ -50,10 +51,18 @@ type Dirent struct {
Type DirentType
}
// Write the supplied directory entry intto the given buffer in the format
// expected in fuseops.ReadFileOp.Data, returning the number of bytes written.
// Write the supplied directory entry into the given buffer in the format
// expected in fuseops.ReadDirOp.Data, returning the number of bytes written.
// Return zero if the entry would not fit.
func WriteDirent(buf []byte, d Dirent) (n int) {
return WriteDirentPlus(buf, nil, d)
}
// Write the supplied directory entry and, optionally, inode entry into the
// given buffer in the format expected in fuseops.ReadDirOp.Data with enabled
// READDIRPLUS capability, returning the number of bytes written.
// Returns zero if the entry would not fit.
func WriteDirentPlus(buf []byte, e *fuseops.ChildInodeEntry, d Dirent) (n int) {
// We want to write bytes with the layout of fuse_dirent
// (http://goo.gl/BmFxob) in host order. The struct must be aligned according
// to FUSE_DIRENT_ALIGN (http://goo.gl/UziWvH), which dictates 8-byte
@ -78,10 +87,21 @@ func WriteDirent(buf []byte, d Dirent) (n int) {
// Do we have enough room?
totalLen := direntSize + len(d.Name) + padLen
if e != nil {
// READDIRPLUS was added in protocol 7.21, entry attributes were added in 7.9
// So here EntryOut is always full-length
totalLen += int(unsafe.Sizeof(fusekernel.EntryOut{}))
}
if totalLen > len(buf) {
return n
}
if e != nil {
out := (*fusekernel.EntryOut)(unsafe.Pointer(&buf[n]))
fuseops.ConvertChildInodeEntry(e, out)
n += int(unsafe.Sizeof(fusekernel.EntryOut{}))
}
// Write the header.
de := fuse_dirent{
ino: uint64(d.Inode),

View File

@ -39,6 +39,7 @@ type FileSystem interface {
GetInodeAttributes(context.Context, *fuseops.GetInodeAttributesOp) error
SetInodeAttributes(context.Context, *fuseops.SetInodeAttributesOp) error
ForgetInode(context.Context, *fuseops.ForgetInodeOp) error
BatchForget(context.Context, *fuseops.BatchForgetOp) error
MkDir(context.Context, *fuseops.MkDirOp) error
MkNode(context.Context, *fuseops.MkNodeOp) error
CreateFile(context.Context, *fuseops.CreateFileOp) error
@ -62,6 +63,9 @@ type FileSystem interface {
ListXattr(context.Context, *fuseops.ListXattrOp) error
SetXattr(context.Context, *fuseops.SetXattrOp) error
Fallocate(context.Context, *fuseops.FallocateOp) error
Poll(context.Context, *fuseops.PollOp) error
SetConnection(*fuse.Connection)
// Regard all inodes (including the root inode) as having their lookup counts
// decremented to zero, and clean up any resources associated with the file
@ -94,6 +98,8 @@ type fileSystemServer struct {
}
func (s *fileSystemServer) ServeOps(c *fuse.Connection) {
s.fs.SetConnection(c)
// When we are done, we clean up by waiting for all in-flight ops then
// destroying the file system.
defer func() {
@ -151,6 +157,22 @@ func (s *fileSystemServer) handleOp(
case *fuseops.ForgetInodeOp:
err = s.fs.ForgetInode(ctx, typed)
case *fuseops.BatchForgetOp:
err = s.fs.BatchForget(ctx, typed)
if err == fuse.ENOSYS {
// Handle as a series of single-inode forget operations
for _, entry := range typed.Entries {
err = s.fs.ForgetInode(ctx, &fuseops.ForgetInodeOp{
Inode: entry.Inode,
N: entry.N,
OpContext: typed.OpContext,
})
if err != nil {
break
}
}
}
case *fuseops.MkDirOp:
err = s.fs.MkDir(ctx, typed)
@ -219,6 +241,9 @@ func (s *fileSystemServer) handleOp(
case *fuseops.FallocateOp:
err = s.fs.Fallocate(ctx, typed)
case *fuseops.PollOp:
err = s.fs.Poll(ctx, typed)
}
c.Reply(ctx, err)

View File

@ -60,6 +60,12 @@ func (fs *NotImplementedFileSystem) ForgetInode(
return fuse.ENOSYS
}
func (fs *NotImplementedFileSystem) BatchForget(
ctx context.Context,
op *fuseops.BatchForgetOp) error {
return fuse.ENOSYS
}
func (fs *NotImplementedFileSystem) MkDir(
ctx context.Context,
op *fuseops.MkDirOp) error {
@ -198,5 +204,14 @@ func (fs *NotImplementedFileSystem) Fallocate(
return fuse.ENOSYS
}
func (fs *NotImplementedFileSystem) Poll(
ctx context.Context,
op *fuseops.PollOp) error {
return fuse.ENOSYS
}
func (fs *NotImplementedFileSystem) SetConnection(*fuse.Connection) {
}
func (fs *NotImplementedFileSystem) Destroy() {
}

7
go.mod
View File

@ -1,6 +1,6 @@
module github.com/jacobsa/fuse
go 1.16
go 1.18
require (
github.com/detailyang/go-fallocate v0.0.0-20180908115635-432fa640bd2e
@ -11,6 +11,7 @@ require (
github.com/jacobsa/syncutil v0.0.0-20180201203307-228ac8e5a6c3
github.com/jacobsa/timeutil v0.0.0-20170205232429-577e5acbbcf6
github.com/kylelemons/godebug v1.1.0
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/net v0.0.0-20220526153639-5463443f8c37
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
)

15
go.sum
View File

@ -15,9 +15,24 @@ github.com/jacobsa/timeutil v0.0.0-20170205232429-577e5acbbcf6/go.mod h1:JEWKD6V
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -42,13 +42,20 @@ func init() {
type InMessage struct {
remaining []byte
storage []byte
size int
}
// NewInMessage creates a new InMessage with its storage initialized.
func NewInMessage() *InMessage {
return &InMessage{
storage: make([]byte, bufSize),
}
}
// Initialize with the data read by a single call to r.Read. The first call to
// Consume will consume the bytes directly after the fusekernel.InHeader
// struct.
func (m *InMessage) Init(r io.Reader) error {
m.storage = make([]byte, bufSize, bufSize)
n, err := r.Read(m.storage[:])
if err != nil {
return err
@ -60,6 +67,7 @@ func (m *InMessage) Init(r io.Reader) error {
return fmt.Errorf("Unexpectedly read only %d bytes.", n)
}
m.size = n
m.remaining = m.storage[headerSize:n]
// Check the header's length.
@ -108,3 +116,11 @@ func (m *InMessage) ConsumeBytes(n uintptr) []byte {
return b
}
// Get the next n bytes after the message to use them as a temporary buffer
func (m *InMessage) GetFree(n int) []byte {
if n <= 0 || n > len(m.storage)-m.size {
return nil
}
return m.storage[m.size : m.size+n]
}

View File

@ -17,4 +17,4 @@ package buffer
// The maximum fuse write request size that InMessage can acommodate.
//
// As of kernel 4.20 Linux accepts writes up to 256 pages or 1MiB
const MaxWriteSize = 1 << 20
const MaxWriteSize = 1 << 17

View File

@ -16,7 +16,6 @@ package buffer
import (
"fmt"
"log"
"reflect"
"unsafe"
@ -33,30 +32,15 @@ const OutMessageHeaderSize = int(unsafe.Sizeof(fusekernel.OutHeader{}))
//
// Must be initialized with Reset.
type OutMessage struct {
// The offset into payload to which we're currently writing.
payloadOffset int
header fusekernel.OutHeader
payload [MaxReadSize]byte
}
// Make sure that the header and payload are contiguous.
func init() {
a := unsafe.Offsetof(OutMessage{}.header) + uintptr(OutMessageHeaderSize)
b := unsafe.Offsetof(OutMessage{}.payload)
if a != b {
log.Panicf(
"header ends at offset %d, but payload starts at offset %d",
a, b)
}
header fusekernel.OutHeader
Sglist [][]byte
}
// Reset resets m so that it's ready to be used again. Afterward, the contents
// are solely a zeroed fusekernel.OutHeader struct.
func (m *OutMessage) Reset() {
m.payloadOffset = 0
m.header = fusekernel.OutHeader{}
m.Sglist = nil
}
// OutHeader returns a pointer to the header at the start of the message.
@ -64,30 +48,12 @@ func (m *OutMessage) OutHeader() *fusekernel.OutHeader {
return &m.header
}
// Grow grows m's buffer by the given number of bytes, returning a pointer to
// the start of the new segment, which is guaranteed to be zeroed. If there is
// insufficient space, it returns nil.
// Grow adds a new buffer of <n> bytes to the message, returning a pointer to
// the start of the new segment, which is guaranteed to be zeroed.
func (m *OutMessage) Grow(n int) unsafe.Pointer {
p := m.GrowNoZero(n)
if p != nil {
jacobsa_fuse_memclr(p, uintptr(n))
}
return p
}
// GrowNoZero is equivalent to Grow, except the new segment is not zeroed. Use
// with caution!
func (m *OutMessage) GrowNoZero(n int) unsafe.Pointer {
// Will we overflow the buffer?
o := m.payloadOffset
if len(m.payload)-o < n {
return nil
}
p := unsafe.Pointer(uintptr(unsafe.Pointer(&m.payload)) + uintptr(o))
m.payloadOffset = o + n
b := make([]byte, n)
m.Append(b)
p := unsafe.Pointer(&b[0])
return p
}
@ -100,51 +66,62 @@ func (m *OutMessage) ShrinkTo(n int) {
n,
m.Len()))
}
m.payloadOffset = n - OutMessageHeaderSize
if n == OutMessageHeaderSize {
m.Sglist = nil
} else {
i := 1
n -= OutMessageHeaderSize
for len(m.Sglist) > i && n >= len(m.Sglist[i]) {
n -= len(m.Sglist[i])
i++
}
if n > 0 {
m.Sglist[i] = m.Sglist[i][0:n]
i++
}
m.Sglist = m.Sglist[0:i]
}
}
// Append is equivalent to growing by len(src), then copying src over the new
// segment. Int panics if there is not enough room available.
func (m *OutMessage) Append(src []byte) {
p := m.GrowNoZero(len(src))
if p == nil {
panic(fmt.Sprintf("Can't grow %d bytes", len(src)))
func (m *OutMessage) Append(src ...[]byte) {
if m.Sglist == nil {
// First element of Sglist is pre-filled with a pointer to the header
// to allow sending it with a single writev() call without copying the
// slice again
m.Sglist = append(m.Sglist, m.OutHeaderBytes())
}
sh := (*reflect.SliceHeader)(unsafe.Pointer(&src))
jacobsa_fuse_memmove(p, unsafe.Pointer(sh.Data), uintptr(sh.Len))
m.Sglist = append(m.Sglist, src...)
return
}
// AppendString is like Append, but accepts string input.
func (m *OutMessage) AppendString(src string) {
p := m.GrowNoZero(len(src))
if p == nil {
panic(fmt.Sprintf("Can't grow %d bytes", len(src)))
}
sh := (*reflect.StringHeader)(unsafe.Pointer(&src))
jacobsa_fuse_memmove(p, unsafe.Pointer(sh.Data), uintptr(sh.Len))
m.Append([]byte(src))
return
}
// Len returns the current size of the message, including the leading header.
func (m *OutMessage) Len() int {
return OutMessageHeaderSize + m.payloadOffset
if m.Sglist == nil {
return OutMessageHeaderSize
}
// First element of Sglist is the header, so we don't need to count it here
r := 0
for _, b := range m.Sglist {
r += len(b)
}
return r
}
// Bytes returns a reference to the current contents of the buffer, including
// the leading header.
func (m *OutMessage) Bytes() []byte {
l := m.Len()
// OutHeaderBytes returns a byte slice containing the current header.
func (m *OutMessage) OutHeaderBytes() []byte {
l := OutMessageHeaderSize
sh := reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(&m.header)),
Len: l,
Cap: l,
}
return *(*[]byte)(unsafe.Pointer(&sh))
}

View File

@ -49,47 +49,6 @@ func findNonZero(p unsafe.Pointer, n int) int {
return n
}
func TestMemclr(t *testing.T) {
// All sizes up to 32 bytes.
var sizes []int
for i := 0; i <= 32; i++ {
sizes = append(sizes, i)
}
// And a few hand-chosen sizes.
sizes = append(sizes, []int{
39, 41, 64, 127, 128, 129,
1<<20 - 1,
1 << 20,
1<<20 + 1,
}...)
// For each size, fill a buffer with random bytes and then zero it.
for _, size := range sizes {
size := size
t.Run(fmt.Sprintf("size=%d", size), func(t *testing.T) {
// Generate
b, err := randBytes(size)
if err != nil {
t.Fatalf("randBytes: %v", err)
}
// Clear
var p unsafe.Pointer
if len(b) != 0 {
p = unsafe.Pointer(&b[0])
}
jacobsa_fuse_memclr(p, uintptr(len(b)))
// Check
if i := findNonZero(p, len(b)); i != len(b) {
t.Fatalf("non-zero byte at offset %d", i)
}
})
}
}
func TestOutMessageAppend(t *testing.T) {
var om OutMessage
om.Reset()
@ -107,9 +66,12 @@ func TestOutMessageAppend(t *testing.T) {
t.Errorf("om.Len() = %d, want %d", got, want)
}
b := om.Bytes()
b := []byte(nil)
for i := 0; i < len(om.Sglist); i++ {
b = append(b, om.Sglist[i]...)
}
if got, want := len(b), wantLen; got != want {
t.Fatalf("len(om.Bytes()) = %d, want %d", got, want)
t.Fatalf("len(om.OutHeaderBytes()) = %d, want %d", got, want)
}
want := append(
@ -137,9 +99,12 @@ func TestOutMessageAppendString(t *testing.T) {
t.Errorf("om.Len() = %d, want %d", got, want)
}
b := om.Bytes()
b := []byte(nil)
for i := 0; i < len(om.Sglist); i++ {
b = append(b, om.Sglist[i]...)
}
if got, want := len(b), wantLen; got != want {
t.Fatalf("len(om.Bytes()) = %d, want %d", got, want)
t.Fatalf("len(om.OutHeaderBytes()) = %d, want %d", got, want)
}
want := append(
@ -168,9 +133,12 @@ func TestOutMessageShrinkTo(t *testing.T) {
t.Errorf("om.Len() = %d, want %d", got, want)
}
b := om.Bytes()
b := []byte(nil)
for i := 0; i < len(om.Sglist); i++ {
b = append(b, om.Sglist[i]...)
}
if got, want := len(b), wantLen; got != want {
t.Fatalf("len(om.Bytes()) = %d, want %d", got, want)
t.Fatalf("len(om.OutHeaderBytes()) = %d, want %d", got, want)
}
want := append(
@ -201,7 +169,7 @@ func TestOutMessageHeader(t *testing.T) {
*h = want
// Check that the result is as expected.
b := om.Bytes()
b := om.OutHeaderBytes()
if len(b) != int(unsafe.Sizeof(want)) {
t.Fatalf("unexpected length %d; want %d", len(b), unsafe.Sizeof(want))
}
@ -225,9 +193,7 @@ func TestOutMessageReset(t *testing.T) {
}
// Ensure a non-zero payload length.
if p := om.GrowNoZero(128); p == nil {
t.Fatal("GrowNoZero failed")
}
om.Grow(128)
// Reset.
om.Reset()
@ -259,10 +225,7 @@ func TestOutMessageGrow(t *testing.T) {
// Set up garbage where the payload will soon be.
const payloadSize = 1234
{
p := om.GrowNoZero(payloadSize)
if p == nil {
t.Fatal("GrowNoZero failed")
}
p := om.Grow(payloadSize)
err := fillWithGarbage(p, payloadSize)
if err != nil {
@ -283,7 +246,10 @@ func TestOutMessageGrow(t *testing.T) {
t.Errorf("om.Len() = %d, want %d", got, want)
}
b := om.Bytes()
b := []byte(nil)
for i := 0; i < len(om.Sglist); i++ {
b = append(b, om.Sglist[i]...)
}
if got, want := len(b), wantLen; got != want {
t.Fatalf("len(om.Len()) = %d, want %d", got, want)
}
@ -304,7 +270,7 @@ func BenchmarkOutMessageReset(b *testing.B) {
om.Reset()
}
b.SetBytes(int64(unsafe.Offsetof(om.payload)))
b.SetBytes(int64(om.Len()))
})
// Many megabytes worth of buffers, which should defeat the CPU cache.
@ -321,7 +287,7 @@ func BenchmarkOutMessageReset(b *testing.B) {
oms[i%numMessages].Reset()
}
b.SetBytes(int64(unsafe.Offsetof(oms[0].payload)))
b.SetBytes(int64(oms[0].Len()))
})
}

View File

@ -1,31 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buffer
import "unsafe"
//go:noescape
// Zero the n bytes starting at p.
//
// REQUIRES: the region does not contain any Go pointers.
//go:linkname jacobsa_fuse_memclr runtime.memclrNoHeapPointers
func jacobsa_fuse_memclr(p unsafe.Pointer, n uintptr)
//go:noescape
// Copy from src to dst, allowing overlap.
//go:linkname jacobsa_fuse_memmove runtime.memmove
func jacobsa_fuse_memmove(dst unsafe.Pointer, src unsafe.Pointer, n uintptr)

View File

@ -1,18 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The buffer package uses //go:linkname to push a few functions into this
// package but we still need a .s file so the Go tool does not pass -complete
// to the go tool compile so the latter does not complain about Go functions
// with no bodies.

View File

@ -168,7 +168,7 @@ const (
// OpenAccessModeMask is a bitmask that separates the access mode
// from the other flags in OpenFlags.
const OpenAccessModeMask OpenFlags = syscall.O_ACCMODE
const OpenAccessModeMask OpenFlags = OpenReadOnly | OpenWriteOnly | OpenReadWrite
// OpenFlags are the O_FOO flags passed to open/create/etc calls. For
// example, os.O_WRONLY | os.O_APPEND.
@ -346,6 +346,34 @@ var releaseFlagNames = []flagName{
{uint32(ReleaseFlush), "ReleaseFlush"},
}
// Poll flags and events are used in the Poll exchange.
type PollFlags uint32
const (
// From the kernel source:
// Ask for notification if there's someone waiting for it.
// The client may ignore the flag and always notify.
PollScheduleNotify PollFlags = 1 << 0
)
type PollEvents uint32
const (
PollInEvent PollEvents = 0x0001
PollPriEvent PollEvents = 0x0002
PollOutEvent PollEvents = 0x0004
PollErrEvent PollEvents = 0x0008
PollHupEvent PollEvents = 0x0010
PollNvalEvent PollEvents = 0x0020
PollRdNormEvent PollEvents = 0x0040
PollRdBandEvent PollEvents = 0x0080
PollWrNormEvent PollEvents = 0x0100
PollWrBandEvent PollEvents = 0x0200
PollMsgEvent PollEvents = 0x0400
PollRemoveEvent PollEvents = 0x1000
PollRdHupEvent PollEvents = 0x2000
)
// Opcodes
const (
OpLookup = 1
@ -386,7 +414,10 @@ const (
OpDestroy = 38
OpIoctl = 39 // Linux?
OpPoll = 40 // Linux?
OpNotifyReply = 41
OpBatchForget = 42
OpFallocate = 43
OpReaddirplus = 44
// OS X
OpSetvolname = 61
@ -417,6 +448,16 @@ type ForgetIn struct {
Nlookup uint64
}
type BatchForgetCountIn struct {
Count uint32
dummy uint32
}
type BatchForgetEntryIn struct {
Inode int64
Nlookup uint64
}
type GetattrIn struct {
GetattrFlags uint32
dummy uint32
@ -541,6 +582,18 @@ func CreateInSize(p Protocol) uintptr {
}
}
type PollIn struct {
Fh uint64
Kh uint64
Flags uint32
Events uint32
}
type PollOut struct {
Revents uint32
padding uint32
}
type ReleaseIn struct {
Fh uint64
Flags uint32
@ -776,8 +829,15 @@ const (
NotifyCodePoll int32 = 1
NotifyCodeInvalInode int32 = 2
NotifyCodeInvalEntry int32 = 3
NotifyCodeStore int32 = 4
NotifyCodeRetrieve int32 = 5
NotifyCodeDelete int32 = 6
)
type NotifyPollWakeupOut struct {
Kh uint64
}
type NotifyInvalInodeOut struct {
Ino uint64
Off int64
@ -789,3 +849,35 @@ type NotifyInvalEntryOut struct {
Namelen uint32
padding uint32
}
type NotifyDeleteOut struct {
Parent uint64
Child uint64
Namelen uint32
padding uint32
}
type NotifyStoreOut struct {
Nodeid uint64
Offset uint64
Size uint32
padding uint32
}
type NotifyRetrieveOut struct {
Unique uint64
Nodeid uint64
Offset uint64
Size uint32
padding uint32
}
// Matches the size of WriteIn
type NotifyRetrieveIn struct {
dummy1 uint64
Offset uint64
Size uint32
dummy2 uint32
dummy3 uint64
dummy4 uint64
}

View File

@ -1,3 +1,5 @@
// +build linux windows
package fusekernel
import "time"

View File

@ -1 +0,0 @@
package fusekernel

155
mount.go
View File

@ -17,7 +17,12 @@ package fuse
import (
"context"
"fmt"
"log"
"net"
"os"
"os/exec"
"strings"
"syscall"
)
// Server is an interface for any type that knows how to serve ops read from a
@ -38,16 +43,8 @@ func Mount(
config *MountConfig) (*MountedFileSystem, error) {
// Sanity check: make sure the mount point exists and is a directory. This
// saves us from some confusing errors later on OS X.
fi, err := os.Stat(dir)
switch {
case os.IsNotExist(err):
if err := checkMountPoint(dir); err != nil {
return nil, err
case err != nil:
return nil, fmt.Errorf("Statting mount point: %v", err)
case !fi.IsDir():
return nil, fmt.Errorf("Mount point %s is not a directory", dir)
}
// Initialize the struct.
@ -57,11 +54,17 @@ func Mount(
}
// Begin the mounting process, which will continue in the background.
if config.DebugLogger != nil {
config.DebugLogger.Println("Beginning the mounting kickoff process")
}
ready := make(chan error, 1)
dev, err := mount(dir, config, ready)
if err != nil {
return nil, fmt.Errorf("mount: %v", err)
}
if config.DebugLogger != nil {
config.DebugLogger.Println("Completed the mounting kickoff process")
}
// Choose a parent context for ops.
cfgCopy := *config
@ -69,6 +72,9 @@ func Mount(
cfgCopy.OpContext = context.Background()
}
if config.DebugLogger != nil {
config.DebugLogger.Println("Creating a connection object")
}
// Create a Connection object wrapping the device.
connection, err := newConnection(
cfgCopy,
@ -78,6 +84,9 @@ func Mount(
if err != nil {
return nil, fmt.Errorf("newConnection: %v", err)
}
if config.DebugLogger != nil {
config.DebugLogger.Println("Successfully created the connection")
}
// Serve the connection in the background. When done, set the join status.
go func() {
@ -86,6 +95,10 @@ func Mount(
close(mfs.joinStatusAvailable)
}()
if config.DebugLogger != nil {
config.DebugLogger.Println("Waiting for mounting process to complete")
}
// Wait for the mount process to complete.
if err := <-ready; err != nil {
return nil, fmt.Errorf("mount (background): %v", err)
@ -93,3 +106,127 @@ func Mount(
return mfs, nil
}
func checkMountPoint(dir string) error {
if strings.HasPrefix(dir, "/dev/fd") {
return nil
}
fi, err := os.Stat(dir)
switch {
case os.IsNotExist(err):
return err
case err != nil:
return fmt.Errorf("Statting mount point: %v", err)
case !fi.IsDir():
return fmt.Errorf("Mount point %s is not a directory", dir)
}
return nil
}
func fusermount(binary string, argv []string, additionalEnv []string, wait bool, debugLogger *log.Logger) (*os.File, error) {
if debugLogger != nil {
debugLogger.Println("Creating a socket pair")
}
// Create a socket pair.
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err != nil {
return nil, fmt.Errorf("Socketpair: %v", err)
}
if debugLogger != nil {
debugLogger.Println("Creating files to wrap the sockets")
}
// Wrap the sockets into os.File objects that we will pass off to fusermount.
writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes")
defer writeFile.Close()
readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads")
defer readFile.Close()
if debugLogger != nil {
debugLogger.Println("Starting fusermount/os mount")
}
// Start fusermount/mount_macfuse/mount_osxfuse.
cmd := exec.Command(binary, argv...)
cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3")
cmd.Env = append(cmd.Env, additionalEnv...)
cmd.ExtraFiles = []*os.File{writeFile}
cmd.Stderr = os.Stderr
// Run the command.
if wait {
err = cmd.Run()
} else {
err = cmd.Start()
}
if err != nil {
return nil, fmt.Errorf("running %v: %v", binary, err)
}
if debugLogger != nil {
debugLogger.Println("Wrapping socket pair in a connection")
}
// Wrap the socket file in a connection.
c, err := net.FileConn(readFile)
if err != nil {
return nil, fmt.Errorf("FileConn: %v", err)
}
defer c.Close()
if debugLogger != nil {
debugLogger.Println("Checking that we have a unix domain socket")
}
// We expect to have a Unix domain socket.
uc, ok := c.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("Expected UnixConn, got %T", c)
}
if debugLogger != nil {
debugLogger.Println("Read a message from socket")
}
// Read a message.
buf := make([]byte, 32) // expect 1 byte
oob := make([]byte, 32) // expect 24 bytes
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
if err != nil {
return nil, fmt.Errorf("ReadMsgUnix: %v", err)
}
// Parse the message.
scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
if err != nil {
return nil, fmt.Errorf("ParseSocketControlMessage: %v", err)
}
// We expect one message.
if len(scms) != 1 {
return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
}
scm := scms[0]
if debugLogger != nil {
debugLogger.Println("Successfully read the socket message.")
}
// Pull out the FD returned by fusermount
gotFds, err := syscall.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err)
}
if len(gotFds) != 1 {
return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds)
}
if debugLogger != nil {
debugLogger.Println("Converting FD into os.File")
}
// Turn the FD into an os.File.
return os.NewFile(uintptr(gotFds[0]), "/dev/fuse"), nil
}

View File

@ -151,11 +151,23 @@ type MountConfig struct {
// OpenDir calls at all (Linux >= 5.1):
EnableNoOpendirSupport bool
// Tell the kernel to use READDIRPLUS.
// Note that the implementation may still fall back to READDIR if the running
// kernel doesn't have support for READDIRPLUS.
UseReadDirPlus bool
// Disable FUSE default permissions.
// This is useful for situations where the backing data store (e.g., S3) doesn't
// actually utilise any form of qualifiable UNIX permissions.
DisableDefaultPermissions bool
// Use vectored reads.
// Vectored read allows file systems to avoid memory copying overhead if
// the data is already in memory when they return it to FUSE.
// When turned on, ReadFileOp.Dst is always nil and the FS must return data
// being read from the file as a list of slices in ReadFileOp.Data.
UseVectoredRead bool
// OS X only.
//
// The name of the mounted volume, as displayed in the Finder. If empty, a
@ -174,6 +186,10 @@ type MountConfig struct {
// /proc/mounts will show the filesystem type as fuse.<Subtype>.
// If not set, /proc/mounts will show the filesystem type as fuse/fuseblk.
Subtype string
// Flag to enable async reads that are received from
// the kernel
EnableAsyncReads bool
}
// Create a map containing all of the key=value mount options to be given to

View File

@ -40,6 +40,10 @@ type osxfuseInstallation struct {
// Environment variable used to pass the "called by library" flag.
LibVar string
// Open device manually (false) or receive the FD through a UNIX socket,
// like with fusermount (true)
UseCommFD bool
}
var (
@ -51,6 +55,7 @@ var (
Mount: "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse",
DaemonVar: "_FUSE_DAEMON_PATH",
LibVar: "_FUSE_CALL_BY_LIB",
UseCommFD: true,
},
// v3
@ -106,6 +111,36 @@ func openOSXFUSEDev(devPrefix string) (dev *os.File, err error) {
}
}
func convertMountArgs(daemonVar string, libVar string,
cfg *MountConfig) ([]string, []string, error) {
// The mount helper doesn't understand any escaping.
for k, v := range cfg.toMap() {
if strings.Contains(k, ",") || strings.Contains(v, ",") {
return nil, nil, fmt.Errorf(
"mount options cannot contain commas on darwin: %q=%q",
k,
v)
}
}
env := []string{libVar + "="}
if daemonVar != "" {
env = append(env, daemonVar+"="+os.Args[0])
}
argv := []string{
"-o", cfg.toOptionsString(),
// Tell osxfuse-kext how large our buffer is. It must split
// writes larger than this into multiple writes.
//
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
// this instead.
"-o", "iosize=" + strconv.FormatUint(buffer.MaxWriteSize, 10),
}
return argv, env, nil
}
func callMount(
bin string,
daemonVar string,
@ -115,39 +150,21 @@ func callMount(
dev *os.File,
ready chan<- error) error {
// The mount helper doesn't understand any escaping.
for k, v := range cfg.toMap() {
if strings.Contains(k, ",") || strings.Contains(v, ",") {
return fmt.Errorf(
"mount options cannot contain commas on darwin: %q=%q",
k,
v)
}
argv, env, err := convertMountArgs(daemonVar, libVar, cfg)
if err != nil {
return err
}
// Call the mount helper, passing in the device file and saving output into a
// buffer.
cmd := exec.Command(
bin,
"-o", cfg.toOptionsString(),
// Tell osxfuse-kext how large our buffer is. It must split
// writes larger than this into multiple writes.
//
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
// this instead.
"-o", "iosize="+strconv.FormatUint(buffer.MaxWriteSize, 10),
argv = append(argv,
// refers to fd passed in cmd.ExtraFiles
"3",
dir,
)
cmd := exec.Command(bin, argv...)
cmd.ExtraFiles = []*os.File{dev}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, libVar+"=")
daemon := os.Args[0]
if daemonVar != "" {
cmd.Env = append(cmd.Env, daemonVar+"="+daemon)
}
cmd.Env = env
var buf bytes.Buffer
cmd.Stdout = &buf
@ -174,6 +191,23 @@ func callMount(
return nil
}
func callMountCommFD(
bin string,
daemonVar string,
libVar string,
dir string,
cfg *MountConfig) (*os.File, error) {
argv, env, err := convertMountArgs(daemonVar, libVar, cfg)
if err != nil {
return nil, err
}
env = append(env, "_FUSE_COMMVERS=2")
argv = append(argv, dir)
return fusermount(bin, argv, env, false, cfg.DebugLogger)
}
// Begin the process of mounting at the given directory, returning a connection
// to the kernel. Mounting continues in the background, and is complete when an
// error is written to the supplied channel. The file system may need to
@ -189,6 +223,16 @@ func mount(
continue
}
if loc.UseCommFD {
// Call the mount binary with the device.
ready <- nil
dev, err = callMountCommFD(loc.Mount, loc.DaemonVar, loc.LibVar, dir, cfg)
if err != nil {
return nil, fmt.Errorf("callMount: %v", err)
}
return
}
// Open the device.
dev, err = openOSXFUSEDev(loc.DevicePrefix)

View File

@ -1,12 +1,12 @@
package fuse
import (
"bytes"
"errors"
"fmt"
"net"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"golang.org/x/sys/unix"
@ -23,92 +23,6 @@ func findFusermount() (string, error) {
return path, nil
}
func fusermount(dir string, cfg *MountConfig) (*os.File, error) {
// Create a socket pair.
fds, err := syscall.Socketpair(syscall.AF_FILE, syscall.SOCK_STREAM, 0)
if err != nil {
return nil, fmt.Errorf("Socketpair: %v", err)
}
// Wrap the sockets into os.File objects that we will pass off to fusermount.
writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes")
defer writeFile.Close()
readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads")
defer readFile.Close()
// Start fusermount, passing it a buffer in which to write stderr.
var stderr bytes.Buffer
fusermount, err := findFusermount()
if err != nil {
return nil, err
}
cmd := exec.Command(
fusermount,
"-o", cfg.toOptionsString(),
"--",
dir,
)
cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3")
cmd.ExtraFiles = []*os.File{writeFile}
cmd.Stderr = &stderr
// Run the command.
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("running fusermount: %v\n\nstderr:\n%s", err, stderr.Bytes())
}
// Wrap the socket file in a connection.
c, err := net.FileConn(readFile)
if err != nil {
return nil, fmt.Errorf("FileConn: %v", err)
}
defer c.Close()
// We expect to have a Unix domain socket.
uc, ok := c.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("Expected UnixConn, got %T", c)
}
// Read a message.
buf := make([]byte, 32) // expect 1 byte
oob := make([]byte, 32) // expect 24 bytes
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
if err != nil {
return nil, fmt.Errorf("ReadMsgUnix: %v", err)
}
// Parse the message.
scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
if err != nil {
return nil, fmt.Errorf("ParseSocketControlMessage: %v", err)
}
// We expect one message.
if len(scms) != 1 {
return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
}
scm := scms[0]
// Pull out the FD returned by fusermount
gotFds, err := syscall.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err)
}
if len(gotFds) != 1 {
return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds)
}
// Turn the FD into an os.File.
return os.NewFile(uintptr(gotFds[0]), "/dev/fuse"), nil
}
func enableFunc(flag uintptr) func(uintptr) uintptr {
return func(v uintptr) uintptr {
return v | flag
@ -141,6 +55,9 @@ var mountflagopts = map[string]func(uintptr) uintptr{
var errFallback = errors.New("sentinel: fallback to fusermount(1)")
func directmount(dir string, cfg *MountConfig) (*os.File, error) {
if cfg.DebugLogger != nil {
cfg.DebugLogger.Println("Preparing for direct mounting")
}
// We use syscall.Open + os.NewFile instead of os.OpenFile so that the file
// is opened in blocking mode. When opened in non-blocking mode, the Go
// runtime tries to use poll(2), which does not work with /dev/fuse.
@ -149,6 +66,10 @@ func directmount(dir string, cfg *MountConfig) (*os.File, error) {
return nil, errFallback
}
dev := os.NewFile(uintptr(fd), "/dev/fuse")
if cfg.DebugLogger != nil {
cfg.DebugLogger.Println("Successfully opened the /dev/fuse in blocking mode")
}
// As per libfuse/fusermount.c:847: https://bit.ly/2SgtWYM#L847
data := fmt.Sprintf("fd=%d,rootmode=40000,user_id=%d,group_id=%d",
dev.Fd(), os.Getuid(), os.Getgid())
@ -170,6 +91,10 @@ func directmount(dir string, cfg *MountConfig) (*os.File, error) {
}
delete(opts, "subtype")
data += "," + mapToOptionsString(opts)
if cfg.DebugLogger != nil {
cfg.DebugLogger.Println("Starting the unix mounting")
}
if err := unix.Mount(
cfg.FSName, // source
dir, // target
@ -183,6 +108,9 @@ func directmount(dir string, cfg *MountConfig) (*os.File, error) {
}
return nil, err
}
if cfg.DebugLogger != nil {
cfg.DebugLogger.Println("Unix mounting completed successfully")
}
return dev, nil
}
@ -194,11 +122,47 @@ func mount(dir string, cfg *MountConfig, ready chan<- error) (*os.File, error) {
// On linux, mounting is never delayed.
ready <- nil
if cfg.DebugLogger != nil {
cfg.DebugLogger.Println("Parsing fuse file descriptor")
}
// If the mountpoint is /dev/fd/N, assume that the file descriptor N is an
// already open FUSE channel. Parse it, cast it to an fd, and don't do any
// other part of the mount dance.
if fd, err := parseFuseFd(dir); err == nil {
dev := os.NewFile(uintptr(fd), "/dev/fuse")
return dev, nil
}
// Try mounting without fusermount(1) first: we might be running as root or
// have the CAP_SYS_ADMIN capability.
dev, err := directmount(dir, cfg)
if err == errFallback {
return fusermount(dir, cfg)
if cfg.DebugLogger != nil {
cfg.DebugLogger.Println("Directmount failed. Trying fallback.")
}
fusermountPath, err := findFusermount()
if err != nil {
return nil, err
}
argv := []string{
"-o", cfg.toOptionsString(),
"--",
dir,
}
return fusermount(fusermountPath, argv, []string{}, true, cfg.DebugLogger)
}
return dev, err
}
func parseFuseFd(dir string) (int, error) {
if !strings.HasPrefix(dir, "/dev/fd/") {
return -1, fmt.Errorf("not a /dev/fd path")
}
fd, err := strconv.ParseUint(strings.TrimPrefix(dir, "/dev/fd/"), 10, 32)
if err != nil {
return -1, fmt.Errorf("invalid /dev/fd/N path: N must be a positive integer")
}
return int(fd), nil
}

37
mount_linux_test.go Normal file
View File

@ -0,0 +1,37 @@
package fuse
import (
"testing"
)
func Test_parseFuseFd(t *testing.T) {
t.Run("valid", func(t *testing.T) {
fd, err := parseFuseFd("/dev/fd/42")
if fd != 42 {
t.Errorf("expected 42, got %d", fd)
}
if err != nil {
t.Errorf("expected no error, got %#v", err)
}
})
t.Run("negative", func(t *testing.T) {
fd, err := parseFuseFd("/dev/fd/-42")
if fd != -1 {
t.Errorf("expected an invalid fd, got %d", fd)
}
if err == nil {
t.Errorf("expected an error, nil")
}
})
t.Run("not an int", func(t *testing.T) {
fd, err := parseFuseFd("/dev/fd/3.14159")
if fd != -1 {
t.Errorf("expected an invalid fd, got %d", fd)
}
if err == nil {
t.Errorf("expected an error, nil")
}
})
}

View File

@ -14,7 +14,10 @@
package fuse
import "context"
import (
"context"
"fmt"
)
// MountedFileSystem represents the status of a mount operation, with a method
// that waits for unmounting.
@ -47,3 +50,17 @@ func (mfs *MountedFileSystem) Join(ctx context.Context) error {
return ctx.Err()
}
}
// GetFuseContext implements the equiv. of FUSE-C fuse_get_context() and thus
// returns the UID / GID / PID associated with all FUSE requests send by the kernel.
// ctx parameter must be one of the context from the fuseops handlers (e.g.: CreateFile)
func (mfs *MountedFileSystem) GetFuseContext(ctx context.Context) (uid, gid, pid uint32, err error) {
foo := ctx.Value(contextKey)
state, ok := foo.(opState)
if !ok {
return 0, 0, 0, fmt.Errorf("GetFuseContext called with invalid context: %#v", ctx)
}
inMsg := state.inMsg
header := inMsg.Header()
return header.Uid, header.Gid, header.Pid, nil
}

View File

@ -36,9 +36,9 @@ const (
// A file system with a fixed structure that looks like this:
//
// foo
// dir/
// bar
// foo
// dir/
// bar
//
// The file system is configured with durations that specify how long to allow
// inode entries and attributes to be cached, used when responding to fuse
@ -71,15 +71,14 @@ type CachingFS interface {
// Create a file system that issues cacheable responses according to the
// following rules:
//
// * LookUpInodeResponse.Entry.EntryExpiration is set according to
// - LookUpInodeResponse.Entry.EntryExpiration is set according to
// lookupEntryTimeout.
//
// * GetInodeAttributesResponse.AttributesExpiration is set according to
// - GetInodeAttributesResponse.AttributesExpiration is set according to
// getattrTimeout.
//
// * Nothing else is marked cacheable. (In particular, the attributes
// - Nothing else is marked cacheable. (In particular, the attributes
// returned by LookUpInode are not cacheable.)
//
func NewCachingFS(
lookupEntryTimeout time.Duration,
getattrTimeout time.Duration) (CachingFS, error) {

View File

@ -28,9 +28,9 @@ import (
// Create a file system with a fixed structure that looks like this:
//
// hello
// dir/
// world
// hello
// dir/
// world
//
// Each file contains the string "Hello, world!".
func NewHelloFS(clock timeutil.Clock) (fuse.Server, error) {

View File

@ -33,6 +33,10 @@ type inode struct {
// Mutable state
/////////////////////////
// Name of the inode, only contains relative path.
// For example, if the full path for an inode is /foo/bar/f1, its name is f1.
name string
// The current attributes of this inode.
//
// INVARIANT: attrs.Mode &^ (os.ModePerm|os.ModeDir|os.ModeSymlink) == 0
@ -73,7 +77,7 @@ type inode struct {
// Create a new inode with the supplied attributes, which need not contain
// time-related information (the inode object will take care of that).
func newInode(attrs fuseops.InodeAttributes) *inode {
func newInode(attrs fuseops.InodeAttributes, name string) *inode {
// Update time info.
now := time.Now()
attrs.Mtime = now
@ -81,6 +85,7 @@ func newInode(attrs fuseops.InodeAttributes) *inode {
// Create the object.
return &inode{
name: name,
attrs: attrs,
xattrs: make(map[string][]byte),
}

View File

@ -16,6 +16,7 @@ package memfs
import (
"context"
"encoding/binary"
"fmt"
"io"
"os"
@ -29,6 +30,11 @@ import (
"golang.org/x/sys/unix"
)
const (
FileOpenFlagsXattrName = "fileOpenFlagsXattr"
CheckFileOpenFlagsFileName = "checkFileOpenFlags"
)
type memFS struct {
fuseutil.NotImplementedFileSystem
@ -85,7 +91,7 @@ func NewMemFS(
Gid: gid,
}
fs.inodes[fuseops.RootInodeID] = newInode(rootAttrs)
fs.inodes[fuseops.RootInodeID] = newInode(rootAttrs, "")
// Set up invariant checking.
fs.mu = syncutil.NewInvariantMutex(fs.checkInvariants)
@ -157,9 +163,9 @@ func (fs *memFS) getInodeOrDie(id fuseops.InodeID) *inode {
//
// LOCKS_REQUIRED(fs.mu)
func (fs *memFS) allocateInode(
attrs fuseops.InodeAttributes) (id fuseops.InodeID, inode *inode) {
attrs fuseops.InodeAttributes, name string) (id fuseops.InodeID, inode *inode) {
// Create the inode.
inode = newInode(attrs)
inode = newInode(attrs, name)
// Re-use a free ID if possible. Otherwise mint a new one.
numFree := len(fs.freeInodes)
@ -194,10 +200,6 @@ func (fs *memFS) StatFS(
func (fs *memFS) LookUpInode(
ctx context.Context,
op *fuseops.LookUpInodeOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -228,10 +230,6 @@ func (fs *memFS) LookUpInode(
func (fs *memFS) GetInodeAttributes(
ctx context.Context,
op *fuseops.GetInodeAttributesOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -251,10 +249,6 @@ func (fs *memFS) GetInodeAttributes(
func (fs *memFS) SetInodeAttributes(
ctx context.Context,
op *fuseops.SetInodeAttributesOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -284,10 +278,6 @@ func (fs *memFS) SetInodeAttributes(
func (fs *memFS) MkDir(
ctx context.Context,
op *fuseops.MkDirOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -310,7 +300,7 @@ func (fs *memFS) MkDir(
}
// Allocate a child.
childID, child := fs.allocateInode(childAttrs)
childID, child := fs.allocateInode(childAttrs, op.Name)
// Add an entry in the parent.
parent.AddChild(childID, op.Name, fuseutil.DT_Directory)
@ -330,10 +320,6 @@ func (fs *memFS) MkDir(
func (fs *memFS) MkNode(
ctx context.Context,
op *fuseops.MkNodeOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -371,7 +357,7 @@ func (fs *memFS) createFile(
}
// Allocate a child.
childID, child := fs.allocateInode(childAttrs)
childID, child := fs.allocateInode(childAttrs, name)
// Add an entry in the parent.
parent.AddChild(childID, name, fuseutil.DT_File)
@ -392,11 +378,6 @@ func (fs *memFS) createFile(
func (fs *memFS) CreateFile(
ctx context.Context,
op *fuseops.CreateFileOp) (err error) {
if op.OpContext.Pid == 0 {
// CreateFileOp should have a valid pid in context.
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -407,10 +388,6 @@ func (fs *memFS) CreateFile(
func (fs *memFS) CreateSymlink(
ctx context.Context,
op *fuseops.CreateSymlinkOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -438,7 +415,7 @@ func (fs *memFS) CreateSymlink(
}
// Allocate a child.
childID, child := fs.allocateInode(childAttrs)
childID, child := fs.allocateInode(childAttrs, op.Name)
// Set up its target.
child.target = op.Target
@ -461,10 +438,6 @@ func (fs *memFS) CreateSymlink(
func (fs *memFS) CreateLink(
ctx context.Context,
op *fuseops.CreateLinkOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -504,10 +477,6 @@ func (fs *memFS) CreateLink(
func (fs *memFS) Rename(
ctx context.Context,
op *fuseops.RenameOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -549,10 +518,6 @@ func (fs *memFS) Rename(
func (fs *memFS) RmDir(
ctx context.Context,
op *fuseops.RmDirOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -585,10 +550,6 @@ func (fs *memFS) RmDir(
func (fs *memFS) Unlink(
ctx context.Context,
op *fuseops.UnlinkOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -616,10 +577,6 @@ func (fs *memFS) Unlink(
func (fs *memFS) OpenDir(
ctx context.Context,
op *fuseops.OpenDirOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -638,10 +595,6 @@ func (fs *memFS) OpenDir(
func (fs *memFS) ReadDir(
ctx context.Context,
op *fuseops.ReadDirOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -657,11 +610,6 @@ func (fs *memFS) ReadDir(
func (fs *memFS) OpenFile(
ctx context.Context,
op *fuseops.OpenFileOp) error {
if op.OpContext.Pid == 0 {
// OpenFileOp should have a valid pid in context.
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -674,16 +622,27 @@ func (fs *memFS) OpenFile(
panic("Found non-file.")
}
if inode.name == CheckFileOpenFlagsFileName {
// For testing purpose only.
// Set attribute (name=fileOpenFlagsXattr, value=OpenFlags) to test whether
// we set OpenFlags correctly. The value is checked in test with getXattr.
value := make([]byte, 4)
binary.LittleEndian.PutUint32(value, uint32(op.OpenFlags))
err := fs.setXattrHelper(inode, &fuseops.SetXattrOp{
Name: FileOpenFlagsXattrName,
Value: value,
})
if err != nil {
panic("unable to set fileOpenFlagsXattr")
}
}
return nil
}
func (fs *memFS) ReadFile(
ctx context.Context,
op *fuseops.ReadFileOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -705,10 +664,6 @@ func (fs *memFS) ReadFile(
func (fs *memFS) WriteFile(
ctx context.Context,
op *fuseops.WriteFileOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -724,20 +679,12 @@ func (fs *memFS) WriteFile(
func (fs *memFS) FlushFile(
ctx context.Context,
op *fuseops.FlushFileOp) (err error) {
if op.OpContext.Pid == 0 {
// FlushFileOp should have a valid pid in context.
return fuse.EINVAL
}
return
}
func (fs *memFS) ReadSymlink(
ctx context.Context,
op *fuseops.ReadSymlinkOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -752,10 +699,6 @@ func (fs *memFS) ReadSymlink(
func (fs *memFS) GetXattr(ctx context.Context,
op *fuseops.GetXattrOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -776,10 +719,6 @@ func (fs *memFS) GetXattr(ctx context.Context,
func (fs *memFS) ListXattr(ctx context.Context,
op *fuseops.ListXattrOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
@ -803,10 +742,6 @@ func (fs *memFS) ListXattr(ctx context.Context,
func (fs *memFS) RemoveXattr(ctx context.Context,
op *fuseops.RemoveXattrOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
inode := fs.getInodeOrDie(op.Inode)
@ -821,14 +756,15 @@ func (fs *memFS) RemoveXattr(ctx context.Context,
func (fs *memFS) SetXattr(ctx context.Context,
op *fuseops.SetXattrOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
inode := fs.getInodeOrDie(op.Inode)
return fs.setXattrHelper(inode, op)
}
// Required to hold fs.mu
func (fs *memFS) setXattrHelper(inode *inode, op *fuseops.SetXattrOp) error {
_, ok := inode.xattrs[op.Name]
switch op.Flags {
@ -850,10 +786,6 @@ func (fs *memFS) SetXattr(ctx context.Context,
func (fs *memFS) Fallocate(ctx context.Context,
op *fuseops.FallocateOp) error {
if op.OpContext.Pid == 0 {
return fuse.EINVAL
}
fs.mu.Lock()
defer fs.mu.Unlock()
inode := fs.getInodeOrDie(op.Inode)

View File

@ -1,3 +1,4 @@
//go:build go1.8
// +build go1.8
package memfs_test

View File

@ -1,3 +1,4 @@
//go:build !go1.8
// +build !go1.8
package memfs_test

View File

@ -16,6 +16,7 @@ package memfs_test
import (
"bytes"
"encoding/binary"
"io"
"io/ioutil"
"os"
@ -31,6 +32,7 @@ import (
fallocate "github.com/detailyang/go-fallocate"
"github.com/jacobsa/fuse"
"github.com/jacobsa/fuse/fusetesting"
"github.com/jacobsa/fuse/internal/fusekernel"
"github.com/jacobsa/fuse/samples"
"github.com/jacobsa/fuse/samples/memfs"
. "github.com/jacobsa/oglematchers"
@ -88,6 +90,15 @@ func applyUmask(m os.FileMode) os.FileMode {
return m &^ os.FileMode(umask)
}
func (t *MemFSTest) checkOpenFlagsXattr(
fileName string, expectedOpenFlags fusekernel.OpenFlags) {
dest := make([]byte, 4)
_, err := unix.Getxattr(fileName, memfs.FileOpenFlagsXattrName, dest)
AssertEq(nil, err)
openFlags := binary.LittleEndian.Uint32(dest)
AssertEq(openFlags, uint32(expectedOpenFlags))
}
////////////////////////////////////////////////////////////////////////
// Boilerplate
////////////////////////////////////////////////////////////////////////
@ -292,7 +303,7 @@ func (t *MemFSTest) CreateNewFile_InRoot() {
var stat *syscall.Stat_t
// Write a file.
fileName := path.Join(t.Dir, "foo")
fileName := path.Join(t.Dir, memfs.CheckFileOpenFlagsFileName)
const contents = "Hello\x00world"
createTime := time.Now()
@ -304,7 +315,7 @@ func (t *MemFSTest) CreateNewFile_InRoot() {
stat = fi.Sys().(*syscall.Stat_t)
AssertEq(nil, err)
ExpectEq("foo", fi.Name())
ExpectEq(memfs.CheckFileOpenFlagsFileName, fi.Name())
ExpectEq(len(contents), fi.Size())
ExpectEq(applyUmask(0400), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIsWithin(createTime, timeSlop))
@ -321,6 +332,7 @@ func (t *MemFSTest) CreateNewFile_InRoot() {
slice, err := ioutil.ReadFile(fileName)
AssertEq(nil, err)
ExpectEq(contents, string(slice))
t.checkOpenFlagsXattr(fileName, fusekernel.OpenReadOnly)
}
func (t *MemFSTest) CreateNewFile_InSubDir() {
@ -372,7 +384,7 @@ func (t *MemFSTest) ModifyExistingFile_InRoot() {
var stat *syscall.Stat_t
// Write a file.
fileName := path.Join(t.Dir, "foo")
fileName := path.Join(t.Dir, memfs.CheckFileOpenFlagsFileName)
createTime := time.Now()
err = ioutil.WriteFile(fileName, []byte("Hello, world!"), 0600)
@ -382,6 +394,7 @@ func (t *MemFSTest) ModifyExistingFile_InRoot() {
f, err := os.OpenFile(fileName, os.O_WRONLY, 0400)
t.ToClose = append(t.ToClose, f)
AssertEq(nil, err)
t.checkOpenFlagsXattr(fileName, fusekernel.OpenWriteOnly)
modifyTime := time.Now()
n, err = f.WriteAt([]byte("H"), 0)
@ -393,7 +406,7 @@ func (t *MemFSTest) ModifyExistingFile_InRoot() {
stat = fi.Sys().(*syscall.Stat_t)
AssertEq(nil, err)
ExpectEq("foo", fi.Name())
ExpectEq(memfs.CheckFileOpenFlagsFileName, fi.Name())
ExpectEq(len("Hello, world!"), fi.Size())
ExpectEq(applyUmask(0600), fi.Mode())
ExpectThat(fi, fusetesting.MtimeIsWithin(modifyTime, timeSlop))
@ -410,6 +423,7 @@ func (t *MemFSTest) ModifyExistingFile_InRoot() {
slice, err := ioutil.ReadFile(fileName)
AssertEq(nil, err)
ExpectEq("Hello, world!", string(slice))
t.checkOpenFlagsXattr(fileName, fusekernel.OpenReadOnly)
}
func (t *MemFSTest) ModifyExistingFile_InSubDir() {
@ -832,7 +846,7 @@ func (t *MemFSTest) AppendMode() {
buf := make([]byte, 1024)
// Create a file with some contents.
fileName := path.Join(t.Dir, "foo")
fileName := path.Join(t.Dir, memfs.CheckFileOpenFlagsFileName)
err = ioutil.WriteFile(fileName, []byte("Jello, "), 0600)
AssertEq(nil, err)
@ -840,6 +854,7 @@ func (t *MemFSTest) AppendMode() {
f, err := os.OpenFile(fileName, os.O_RDWR|os.O_APPEND, 0600)
t.ToClose = append(t.ToClose, f)
AssertEq(nil, err)
t.checkOpenFlagsXattr(fileName, fusekernel.OpenReadWrite)
// Seek to somewhere silly and then write.
off, err = f.Seek(2, 0)
@ -855,10 +870,6 @@ func (t *MemFSTest) AppendMode() {
AssertEq(nil, err)
ExpectEq(13, off)
off, err = getFileOffset(f)
AssertEq(nil, err)
ExpectEq(13, off)
// Read back the contents of the file, which should be correct even though we
// seeked to a silly place before writing the world part.
//
@ -911,7 +922,7 @@ func (t *MemFSTest) ReadsPastEndOfFile() {
func (t *MemFSTest) Truncate_Smaller() {
var err error
fileName := path.Join(t.Dir, "foo")
fileName := path.Join(t.Dir, memfs.CheckFileOpenFlagsFileName)
// Create a file.
err = ioutil.WriteFile(fileName, []byte("taco"), 0600)
@ -921,6 +932,7 @@ func (t *MemFSTest) Truncate_Smaller() {
f, err := os.OpenFile(fileName, os.O_RDWR, 0)
t.ToClose = append(t.ToClose, f)
AssertEq(nil, err)
t.checkOpenFlagsXattr(fileName, fusekernel.OpenReadWrite)
// Truncate it.
err = f.Truncate(2)
@ -935,6 +947,7 @@ func (t *MemFSTest) Truncate_Smaller() {
contents, err := ioutil.ReadFile(fileName)
AssertEq(nil, err)
ExpectEq("ta", string(contents))
t.checkOpenFlagsXattr(fileName, fusekernel.OpenReadOnly)
}
func (t *MemFSTest) Truncate_SameSize() {

View File

@ -0,0 +1,68 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"flag"
"log"
"os/user"
"strconv"
"github.com/jacobsa/fuse"
"github.com/jacobsa/fuse/samples/memfs"
)
var fMountPoint = flag.String("mount_point", "", "Path to mount point.")
func main() {
flag.Parse()
if *fMountPoint == "" {
log.Fatalf("You must set --mount_point.")
}
user, err := user.Current()
if err != nil {
panic(err)
}
uid, err := strconv.ParseUint(user.Uid, 10, 32)
if err != nil {
panic(err)
}
gid, err := strconv.ParseUint(user.Gid, 10, 32)
if err != nil {
panic(err)
}
server := memfs.NewMemFS(uint32(uid), uint32(gid))
cfg := &fuse.MountConfig{
// Disable writeback caching so that pid is always available in OpContext
DisableWritebackCaching: true,
}
mfs, err := fuse.Mount(*fMountPoint, server, cfg)
if err != nil {
log.Fatalf("Mount: %v", err)
}
// Wait for it to be unmounted.
if err = mfs.Join(context.Background()); err != nil {
log.Fatalf("Join: %v", err)
}
}

View File

@ -0,0 +1,58 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/jacobsa/fuse"
"github.com/jacobsa/fuse/samples/readbenchfs"
"log"
"net/http"
_ "net/http/pprof"
"os"
)
var fMountPoint = flag.String("mount_point", "", "Path to mount point.")
var fReadOnly = flag.Bool("read_only", false, "Mount in read-only mode.")
var fVectored = flag.Bool("vectored", false, "Use vectored read.")
var fDebug = flag.Bool("debug", false, "Enable debug logging.")
var fPprof = flag.Int("pprof", 0, "Enable pprof profiling on the specified port.")
func main() {
flag.Parse()
if *fPprof != 0 {
go func() {
fmt.Printf("%v", http.ListenAndServe(fmt.Sprintf("localhost:%v", *fPprof), nil))
}()
}
server, err := readbenchfs.NewReadBenchServer(*fVectored)
if err != nil {
log.Fatalf("makeFS: %v", err)
}
// Mount the file system.
if *fMountPoint == "" {
log.Fatalf("You must set --mount_point.")
}
cfg := &fuse.MountConfig{
ReadOnly: *fReadOnly,
UseVectoredRead: *fVectored,
}
if *fDebug {
cfg.DebugLogger = log.New(os.Stderr, "fuse: ", 0)
}
mfs, err := fuse.Mount(*fMountPoint, server, cfg)
if err != nil {
log.Fatalf("Mount: %v", err)
}
// Wait for it to be unmounted.
if err = mfs.Join(context.Background()); err != nil {
log.Fatalf("Join: %v", err)
}
}

View File

@ -0,0 +1,170 @@
// Copyright 2021 Vitaliy Filippov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readbenchfs
import (
"golang.org/x/net/context"
"io"
"math/rand"
"os"
"github.com/jacobsa/fuse"
"github.com/jacobsa/fuse/fuseops"
"github.com/jacobsa/fuse/fuseutil"
)
type readBenchFS struct {
fuseutil.NotImplementedFileSystem
buf []byte
useVectoredRead bool
}
// 1 TB
const fileSize = 1024 * 1024 * 1024 * 1024
var _ fuseutil.FileSystem = &readBenchFS{}
func NewReadBenchServer(useVectoredRead bool) (server fuse.Server, err error) {
// 1 GB of random data to exceed CPU cache
buf := make([]byte, 1024*1024*1024)
rand.Read(buf)
server = fuseutil.NewFileSystemServer(&readBenchFS{
buf: buf,
useVectoredRead: useVectoredRead,
})
return
}
func (fs *readBenchFS) StatFS(ctx context.Context, op *fuseops.StatFSOp) error {
return nil
}
func (fs *readBenchFS) LookUpInode(ctx context.Context, op *fuseops.LookUpInodeOp) error {
if op.Name == "test" {
op.Entry = fuseops.ChildInodeEntry{
Child: 2,
Attributes: fuseops.InodeAttributes{
Size: fileSize,
Nlink: 1,
Mode: 0444,
},
}
return nil
}
return fuse.ENOENT
}
func (fs *readBenchFS) GetInodeAttributes(ctx context.Context, op *fuseops.GetInodeAttributesOp) error {
if op.Inode == 1 {
op.Attributes = fuseops.InodeAttributes{
Nlink: 1,
Mode: 0755 | os.ModeDir,
}
return nil
} else if op.Inode == 2 {
op.Attributes = fuseops.InodeAttributes{
Size: fileSize,
Nlink: 1,
Mode: 0444,
}
return nil
}
return fuse.ENOENT
}
func (fs *readBenchFS) OpenDir(ctx context.Context, op *fuseops.OpenDirOp) error {
// Allow opening any directory.
return nil
}
func (fs *readBenchFS) ReadDir(ctx context.Context, op *fuseops.ReadDirOp) error {
if op.Inode != 1 {
return fuse.ENOENT
}
if op.Offset > 0 {
return nil
}
entries := []fuseutil.Dirent{
fuseutil.Dirent{
Offset: 1,
Inode: 2,
Name: "test",
Type: fuseutil.DT_File,
},
}
for _, e := range entries[op.Offset:] {
n := fuseutil.WriteDirent(op.Dst[op.BytesRead:], e)
if n == 0 {
break
}
op.BytesRead += n
}
return nil
}
func (fs *readBenchFS) OpenFile(ctx context.Context, op *fuseops.OpenFileOp) error {
// Allow opening any file.
return nil
}
func (fs *readBenchFS) ReadFile(ctx context.Context, op *fuseops.ReadFileOp) error {
if op.Offset > fileSize {
return io.EOF
}
end := op.Offset + op.Size
if end > fileSize {
end = fileSize
}
buflen := int64(len(fs.buf))
for pos := op.Offset; pos < end; {
s := pos % buflen
e := buflen
if e-s > end-pos {
e = s + end - pos
}
if fs.useVectoredRead {
op.Data = append(op.Data, fs.buf[s:e])
} else {
copy(op.Dst[pos-op.Offset:], fs.buf[s:])
}
pos = op.Offset + e
}
op.BytesRead = int(end - op.Offset)
return nil
}
func (fs *readBenchFS) ReleaseDirHandle(ctx context.Context, op *fuseops.ReleaseDirHandleOp) error {
return nil
}
func (fs *readBenchFS) GetXattr(ctx context.Context, op *fuseops.GetXattrOp) error {
return nil
}
func (fs *readBenchFS) ListXattr(ctx context.Context, op *fuseops.ListXattrOp) error {
return nil
}
func (fs *readBenchFS) ForgetInode(ctx context.Context, op *fuseops.ForgetInodeOp) error {
return nil
}
func (fs *readBenchFS) ReleaseFileHandle(ctx context.Context, op *fuseops.ReleaseFileHandleOp) error {
return nil
}
func (fs *readBenchFS) FlushFile(ctx context.Context, op *fuseops.FlushFileOp) error {
return nil
}

View File

@ -26,9 +26,8 @@ import (
// Sample output:
//
// Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on
// fake@bucket 32 16 16 50% 0 0 100% /Users/jacobsa/tmp/mp
//
// Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on
// fake@bucket 32 16 16 50% 0 0 100% /Users/jacobsa/tmp/mp
var gDfOutputRegexp = regexp.MustCompile(`^\S+\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%\s+\d+\s+\d+\s+\d+%.*$`)
////////////////////////////////////////////////////////////////////////

View File

@ -26,9 +26,8 @@ import (
// Sample output:
//
// Filesystem 1K-blocks Used Available Use% Mounted on
// some_fuse_file_system 512 64 384 15% /tmp/sample_test001288095
//
// Filesystem 1K-blocks Used Available Use% Mounted on
// some_fuse_file_system 512 64 384 15% /tmp/sample_test001288095
var gDfOutputRegexp = regexp.MustCompile(`^\S+\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%.*$`)
////////////////////////////////////////////////////////////////////////

View File

@ -1,3 +1,4 @@
//go:build !linux
// +build !linux
package fuse

29
writev.go Normal file
View File

@ -0,0 +1,29 @@
package fuse
import (
"syscall"
"unsafe"
)
func writev(fd int, packet [][]byte) (n int, err error) {
iovecs := make([]syscall.Iovec, 0, len(packet))
for _, v := range packet {
if len(v) == 0 {
continue
}
vec := syscall.Iovec{
Base: &v[0],
}
vec.SetLen(len(v))
iovecs = append(iovecs, vec)
}
n1, _, e1 := syscall.Syscall(
syscall.SYS_WRITEV,
uintptr(fd), uintptr(unsafe.Pointer(&iovecs[0])), uintptr(len(iovecs)),
)
n = int(n1)
if e1 != 0 {
err = syscall.Errno(e1)
}
return
}