Don't depend on fuseshim for mounting.

geesefs-0-30-9
Aaron Jacobs 2015-07-24 16:09:46 +10:00
commit 9b4cfc818e
5 changed files with 359 additions and 143 deletions

View File

@ -32,6 +32,25 @@ import (
"github.com/jacobsa/fuse/internal/fuseshim"
)
// Ask the Linux kernel for larger read requests.
//
// As of 2015-03-26, the behavior in the kernel is:
//
// * (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
// 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
// how much to read ahead.
//
// * (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
// A connection to the fuse kernel process.
type Connection struct {
debugLogger *log.Logger
@ -58,15 +77,27 @@ type Connection struct {
cancelFuncs map[uint64]func()
}
// Responsibility for closing the wrapped connection is transferred to the
// result. You must call c.close() eventually.
// Create a connection wrapping the supplied file descriptor connected to the
// kernel. You must eventually call c.close().
//
// The loggers may be nil.
func newConnection(
parentCtx context.Context,
debugLogger *log.Logger,
errorLogger *log.Logger,
wrapped *fuseshim.Conn) (c *Connection, err error) {
dev *os.File) (c *Connection, err error) {
// Create an initialized a wrapped fuseshim connection.
wrapped := &fuseshim.Conn{
Dev: dev,
}
err = fuseshim.InitMount(wrapped, maxReadahead, 0)
if err != nil {
err = fmt.Errorf("fuseshim.InitMount: %v", err)
return
}
// Create an object wrapping it.
c = &Connection{
debugLogger: debugLogger,
errorLogger: errorLogger,

View File

@ -164,7 +164,7 @@ func Mount(dir string, options ...MountOption) (*Conn, error) {
}
c.Dev = f
if err := initMount(c, &conf); err != nil {
if err := InitMount(c, conf.maxReadahead, conf.initFlags); err != nil {
c.Close()
return nil, err
}
@ -181,7 +181,10 @@ func (e *OldVersionError) Error() string {
return fmt.Sprintf("kernel FUSE version is too old: %v < %v", e.Kernel, e.LibraryMin)
}
func initMount(c *Conn, conf *mountConfig) error {
func InitMount(
c *Conn,
maxReadahead uint32,
initFlags fusekernel.InitFlags) error {
req, err := c.ReadRequest()
if err != nil {
if err == io.EOF {
@ -213,9 +216,9 @@ func initMount(c *Conn, conf *mountConfig) error {
s := &InitResponse{
Library: proto,
MaxReadahead: conf.maxReadahead,
MaxReadahead: maxReadahead,
MaxWrite: maxWrite,
Flags: fusekernel.InitBigWrites | conf.initFlags,
Flags: fusekernel.InitBigWrites | initFlags,
}
r.Respond(s)
return nil

153
mount_config.go Normal file
View File

@ -0,0 +1,153 @@
// 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 fuse
import (
"fmt"
"log"
"runtime"
"strings"
"golang.org/x/net/context"
)
// Optional configuration accepted by Mount.
type MountConfig struct {
// The context from which every op read from the connetion by the sever
// should inherit. If nil, context.Background() will be used.
OpContext context.Context
// If non-empty, the name of the file system as displayed by e.g. `mount`.
// This is important because the `umount` command requires root privileges if
// it doesn't agree with /etc/fstab.
FSName string
// Mount the file system in read-only mode. File modes will appear as normal,
// but opening a file for writing and metadata operations like chmod,
// chtimes, etc. will fail.
ReadOnly bool
// A logger to use for logging errors. All errors are logged, with the
// exception of a few blacklisted errors that are expected. If nil, no error
// logging is performed.
ErrorLogger *log.Logger
// A logger to use for logging debug information. If nil, no debug logging is
// performed.
DebugLogger *log.Logger
// OS X only.
//
// Normally on OS X we mount with the novncache option
// (cf. http://goo.gl/1pTjuk), which disables entry caching in the kernel.
// This is because osxfuse does not honor the entry expiration values we
// return to it, instead caching potentially forever (cf.
// http://goo.gl/8yR0Ie), and it is probably better to fail to cache than to
// cache for too long, since the latter is more likely to hide consistency
// bugs that are difficult to detect and diagnose.
//
// This field disables the use of novncache, restoring entry caching. Beware:
// the value of ChildInodeEntry.EntryExpiration is ignored by the kernel, and
// entries will be cached for an arbitrarily long time.
EnableVnodeCaching bool
// Additional key=value options to pass unadulterated to the underlying mount
// command. See `man 8 mount`, the fuse documentation, etc. for
// system-specific information.
//
// For expert use only! May invalidate other guarantees made in the
// documentation for this package.
Options map[string]string
}
// Create a map containing all of the key=value mount options to be given to
// the mount helper.
func (c *MountConfig) toMap() (opts map[string]string) {
isDarwin := runtime.GOOS == "darwin"
opts = make(map[string]string)
// Enable permissions checking in the kernel. See the comments on
// InodeAttributes.Mode.
opts["default_permissions"] = ""
// HACK(jacobsa): Work around what appears to be a bug in systemd v219, as
// shipped in Ubuntu 15.04, where it automatically unmounts any file system
// that doesn't set an explicit name.
//
// When Ubuntu contains systemd v220, this workaround should be removed and
// the systemd bug reopened if the problem persists.
//
// Cf. https://github.com/bazil/fuse/issues/89
// Cf. https://bugs.freedesktop.org/show_bug.cgi?id=90907
fsname := c.FSName
if runtime.GOOS == "linux" && fsname == "" {
fsname = "some_fuse_file_system"
}
// Special file system name?
if fsname != "" {
opts["fsname"] = fsname
}
// Read only?
if c.ReadOnly {
opts["ro"] = ""
}
// OS X: set novncache when appropriate.
if isDarwin && !c.EnableVnodeCaching {
opts["novncache"] = ""
}
// OS X: disable the use of "Apple Double" (._foo and .DS_Store) files, which
// just add noise to debug output and can have significant cost on
// network-based file systems.
//
// Cf. https://github.com/osxfuse/osxfuse/wiki/Mount-options
if isDarwin {
opts["noappledouble"] = ""
}
// Last but not least: other user-supplied options.
for k, v := range c.Options {
opts[k] = v
}
return
}
func escapeOptionsKey(s string) (res string) {
res = s
res = strings.Replace(res, `\`, `\\`, -1)
res = strings.Replace(res, `,`, `\,`, -1)
return
}
// Create an options string suitable for passing to the mount helper.
func (c *MountConfig) toOptionsString() string {
var components []string
for k, v := range c.toMap() {
k = escapeOptionsKey(k)
component := k
if v != "" {
component = fmt.Sprintf("%s=%s", k, v)
}
components = append(components, component)
}
return strings.Join(components, ",")
}

156
mount_darwin.go Normal file
View File

@ -0,0 +1,156 @@
package fuse
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"github.com/jacobsa/fuse/internal/buffer"
)
var errNoAvail = errors.New("no available fuse devices")
var errNotLoaded = errors.New("osxfusefs is not loaded")
func loadOSXFUSE() error {
cmd := exec.Command("/Library/Filesystems/osxfusefs.fs/Support/load_osxfusefs")
cmd.Dir = "/"
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
return err
}
func openOSXFUSEDev() (dev *os.File, err error) {
// Try each device name.
for i := uint64(0); ; i++ {
path := fmt.Sprintf("/dev/osxfuse%d", i)
dev, err = os.OpenFile(path, os.O_RDWR, 0000)
if os.IsNotExist(err) {
if i == 0 {
// Not even the first device was found. Fuse must not be loaded.
err = errNotLoaded
return
}
// Otherwise we've run out of kernel-provided devices
err = errNoAvail
return
}
if err2, ok := err.(*os.PathError); ok && err2.Err == syscall.EBUSY {
// This device is in use; try the next one.
continue
}
return
}
}
func callMount(
dir string,
cfg *MountConfig,
dev *os.File,
ready chan<- error) (err error) {
const bin = "/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs"
// 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)
}
}
// 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),
// refers to fd passed in cmd.ExtraFiles
"3",
dir,
)
cmd.ExtraFiles = []*os.File{dev}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "MOUNT_FUSEFS_CALL_BY_LIB=")
// TODO this is used for fs typenames etc, let app influence it
cmd.Env = append(cmd.Env, "MOUNT_FUSEFS_DAEMON_PATH="+bin)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
err = cmd.Start()
if err != nil {
return
}
// In the background, wait for the command to complete.
go func() {
err := cmd.Wait()
if err != nil {
if buf.Len() > 0 {
output := buf.Bytes()
output = bytes.TrimRight(output, "\n")
err = fmt.Errorf("%v: %s", err, output)
}
}
ready <- err
}()
return
}
// 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
// service the connection in order for mounting to complete.
func mount(
dir string,
cfg *MountConfig,
ready chan<- error) (dev *os.File, err error) {
// Open the device.
dev, err = openOSXFUSEDev()
// Special case: we may need to explicitly load osxfuse. Load it, then try
// again.
if err == errNotLoaded {
err = loadOSXFUSE()
if err != nil {
err = fmt.Errorf("loadOSXFUSE: %v", err)
return
}
dev, err = openOSXFUSEDev()
}
// Propagate errors.
if err != nil {
err = fmt.Errorf("openOSXFUSEDev: %v", err)
return
}
// Call the mount binary with the device.
err = callMount(dir, cfg, dev, ready)
if err != nil {
dev.Close()
err = fmt.Errorf("callMount: %v", err)
return
}
return
}

View File

@ -16,10 +16,6 @@ package fuse
import (
"fmt"
"log"
"runtime"
"github.com/jacobsa/fuse/internal/fuseshim"
"golang.org/x/net/context"
)
@ -63,129 +59,6 @@ func (mfs *MountedFileSystem) Join(ctx context.Context) error {
}
}
// Optional configuration accepted by Mount.
type MountConfig struct {
// The context from which every op read from the connetion by the sever
// should inherit. If nil, context.Background() will be used.
OpContext context.Context
// If non-empty, the name of the file system as displayed by e.g. `mount`.
// This is important because the `umount` command requires root privileges if
// it doesn't agree with /etc/fstab.
FSName string
// Mount the file system in read-only mode. File modes will appear as normal,
// but opening a file for writing and metadata operations like chmod,
// chtimes, etc. will fail.
ReadOnly bool
// A logger to use for logging errors. All errors are logged, with the
// exception of a few blacklisted errors that are expected. If nil, no error
// logging is performed.
ErrorLogger *log.Logger
// A logger to use for logging debug information. If nil, no debug logging is
// performed.
DebugLogger *log.Logger
// OS X only.
//
// Normally on OS X we mount with the novncache option
// (cf. http://goo.gl/1pTjuk), which disables entry caching in the kernel.
// This is because osxfuse does not honor the entry expiration values we
// return to it, instead caching potentially forever (cf.
// http://goo.gl/8yR0Ie), and it is probably better to fail to cache than to
// cache for too long, since the latter is more likely to hide consistency
// bugs that are difficult to detect and diagnose.
//
// This field disables the use of novncache, restoring entry caching. Beware:
// the value of ChildInodeEntry.EntryExpiration is ignored by the kernel, and
// entries will be cached for an arbitrarily long time.
EnableVnodeCaching bool
// Additional key=value options to pass unadulterated to the underlying mount
// command. See `man 8 mount`, the fuse documentation, etc. for
// system-specific information.
//
// For expert use only! May invalidate other guarantees made in the
// documentation for this package.
Options map[string]string
}
// Convert to mount options to be passed to package fuseshim.
func (c *MountConfig) bazilfuseOptions() (opts []fuseshim.MountOption) {
isDarwin := runtime.GOOS == "darwin"
// Enable permissions checking in the kernel. See the comments on
// InodeAttributes.Mode.
opts = append(opts, fuseshim.SetOption("default_permissions", ""))
// HACK(jacobsa): Work around what appears to be a bug in systemd v219, as
// shipped in Ubuntu 15.04, where it automatically unmounts any file system
// that doesn't set an explicit name.
//
// When Ubuntu contains systemd v220, this workaround should be removed and
// the systemd bug reopened if the problem persists.
//
// Cf. https://github.com/bazil/fuse/issues/89
// Cf. https://bugs.freedesktop.org/show_bug.cgi?id=90907
fsname := c.FSName
if runtime.GOOS == "linux" && fsname == "" {
fsname = "some_fuse_file_system"
}
// Special file system name?
if fsname != "" {
opts = append(opts, fuseshim.FSName(fsname))
}
// Read only?
if c.ReadOnly {
opts = append(opts, fuseshim.ReadOnly())
}
// OS X: set novncache when appropriate.
if isDarwin && !c.EnableVnodeCaching {
opts = append(opts, fuseshim.SetOption("novncache", ""))
}
// OS X: disable the use of "Apple Double" (._foo and .DS_Store) files, which
// just add noise to debug output and can have significant cost on
// network-based file systems.
//
// Cf. https://github.com/osxfuse/osxfuse/wiki/Mount-options
if isDarwin {
opts = append(opts, fuseshim.SetOption("noappledouble", ""))
}
// Ask the Linux kernel for larger read requests.
//
// As of 2015-03-26, the behavior in the kernel is:
//
// * (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
// 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
// how much to read ahead.
//
// * (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
opts = append(opts, fuseshim.MaxReadahead(maxReadahead))
// Last but not least: other user-supplied options.
for k, v := range c.Options {
opts = append(opts, fuseshim.SetOption(k, v))
}
return
}
// Attempt to mount a file system on the given directory, using the supplied
// Server to serve connection requests. This function blocks until the file
// system is successfully mounted.
@ -199,10 +72,11 @@ func Mount(
joinStatusAvailable: make(chan struct{}),
}
// Open a fuseshim connection.
bfConn, err := fuseshim.Mount(mfs.dir, config.bazilfuseOptions()...)
// Begin the mounting process, which will continue in the background.
ready := make(chan error, 1)
dev, err := mount(dir, config, ready)
if err != nil {
err = fmt.Errorf("fuseshim.Mount: %v", err)
err = fmt.Errorf("mount: %v", err)
return
}
@ -212,15 +86,14 @@ func Mount(
opContext = context.Background()
}
// Create our own Connection object wrapping it.
// Create a Connection object wrapping the device.
connection, err := newConnection(
opContext,
config.DebugLogger,
config.ErrorLogger,
bfConn)
dev)
if err != nil {
bfConn.Close()
err = fmt.Errorf("newConnection: %v", err)
return
}
@ -232,9 +105,9 @@ func Mount(
close(mfs.joinStatusAvailable)
}()
// Wait for the connection to say it is ready.
if err = connection.waitForReady(); err != nil {
err = fmt.Errorf("WaitForReady: %v", err)
// Wait for the mount process to complete.
if err = <-ready; err != nil {
err = fmt.Errorf("mount (background): %v", err)
return
}