diff --git a/connection.go b/connection.go index 4e83b8b..b136e06 100644 --- a/connection.go +++ b/connection.go @@ -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, diff --git a/internal/fuseshim/fuse.go b/internal/fuseshim/fuse.go index 22fde82..8b2d3a1 100644 --- a/internal/fuseshim/fuse.go +++ b/internal/fuseshim/fuse.go @@ -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 diff --git a/mount_config.go b/mount_config.go new file mode 100644 index 0000000..dd76561 --- /dev/null +++ b/mount_config.go @@ -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, ",") +} diff --git a/mount_darwin.go b/mount_darwin.go new file mode 100644 index 0000000..428b73a --- /dev/null +++ b/mount_darwin.go @@ -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 +} diff --git a/mounted_file_system.go b/mounted_file_system.go index 4a57ec6..54532bb 100644 --- a/mounted_file_system.go +++ b/mounted_file_system.go @@ -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 }