Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MacFuse >= 4.0 #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ package fuse
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"syscall"
)

// Server is an interface for any type that knows how to serve ops read from a
Expand Down Expand Up @@ -93,3 +96,82 @@ func Mount(

return mfs, nil
}

func fusermount(binary string, argv []string, additionalEnv []string, wait bool) (*os.File, error) {
// 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)
}

// 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/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)
}

// 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
}
104 changes: 80 additions & 24 deletions mount_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,34 @@ type osxfuseInstallation struct {
// Environment variable used to pass the path to the executable calling the
// mount helper.
DaemonVar string

// 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 (
osxfuseInstallations = []osxfuseInstallation{
// v4
{
DevicePrefix: "/dev/macfuse",
Load: "/Library/Filesystems/macfuse.fs/Contents/Resources/load_macfuse",
Mount: "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse",
DaemonVar: "_FUSE_DAEMON_PATH",
LibVar: "_FUSE_CALL_BY_LIB",
UseCommFD: true,
},

// v3
{
DevicePrefix: "/dev/osxfuse",
Load: "/Library/Filesystems/osxfuse.fs/Contents/Resources/load_osxfuse",
Mount: "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse",
DaemonVar: "MOUNT_OSXFUSE_DAEMON_PATH",
LibVar: "MOUNT_OSXFUSE_CALL_BY_LIB",
},

// v2
Expand All @@ -55,6 +73,7 @@ var (
Load: "/Library/Filesystems/osxfusefs.fs/Support/load_osxfusefs",
Mount: "/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs",
DaemonVar: "MOUNT_FUSEFS_DAEMON_PATH",
LibVar: "MOUNT_FUSEFS_CALL_BY_LIB",
},
}
)
Expand Down Expand Up @@ -92,50 +111,60 @@ func openOSXFUSEDev(devPrefix string) (dev *os.File, err error) {
}
}

func callMount(
bin string,
daemonVar string,
dir string,
cfg *MountConfig,
dev *os.File,
ready chan<- error) 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 fmt.Errorf(
return nil, nil, 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,
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),
"-o", "iosize=" + strconv.FormatUint(buffer.MaxWriteSize, 10),
}

return argv, env, nil
}

func callMount(
bin string,
daemonVar string,
libVar string,
dir string,
cfg *MountConfig,
dev *os.File,
ready chan<- error) error {

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.
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()
// OSXFUSE <3.3.0
cmd.Env = append(cmd.Env, "MOUNT_FUSEFS_CALL_BY_LIB=")
// OSXFUSE >=3.3.0
cmd.Env = append(cmd.Env, "MOUNT_OSXFUSE_CALL_BY_LIB=")

daemon := os.Args[0]
if daemonVar != "" {
cmd.Env = append(cmd.Env, daemonVar+"="+daemon)
}
cmd.Env = env

var buf bytes.Buffer
cmd.Stdout = &buf
Expand All @@ -162,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)
}

// 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
Expand All @@ -177,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)

Expand All @@ -197,7 +253,7 @@ func mount(
}

// Call the mount binary with the device.
if err := callMount(loc.Mount, loc.DaemonVar, dir, cfg, dev, ready); err != nil {
if err := callMount(loc.Mount, loc.DaemonVar, loc.LibVar, dir, cfg, dev, ready); err != nil {
dev.Close()
return nil, fmt.Errorf("callMount: %v", err)
}
Expand Down
96 changes: 10 additions & 86 deletions mount_linux.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,14 @@
package fuse

import (
"bytes"
"errors"
"fmt"
"net"
"os"
"os/exec"
"syscall"

"golang.org/x/sys/unix"
)

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

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
Expand Down Expand Up @@ -183,7 +98,16 @@ func mount(dir string, cfg *MountConfig, ready chan<- error) (*os.File, error) {
// have the CAP_SYS_ADMIN capability.
dev, err := directmount(dir, cfg)
if err == errFallback {
return fusermount(dir, cfg)
fusermountPath, err := findFusermount()
if err != nil {
return nil, err
}
argv := []string{
"-o", cfg.toOptionsString(),
"--",
dir,
}
return fusermount(fusermountPath, argv, []string{}, true)
}
return dev, err
}