forked from TrueCloudLab/restic
208 lines
5.1 KiB
Go
208 lines
5.1 KiB
Go
package fuse
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
)
|
|
|
|
var (
|
|
errNoAvail = errors.New("no available fuse devices")
|
|
errNotLoaded = errors.New("osxfuse is not loaded")
|
|
)
|
|
|
|
func loadOSXFUSE(bin string) error {
|
|
cmd := exec.Command(bin)
|
|
cmd.Dir = "/"
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err := cmd.Run()
|
|
return err
|
|
}
|
|
|
|
func openOSXFUSEDev(devPrefix string) (*os.File, error) {
|
|
var f *os.File
|
|
var err error
|
|
for i := uint64(0); ; i++ {
|
|
path := devPrefix + strconv.FormatUint(i, 10)
|
|
f, err = os.OpenFile(path, os.O_RDWR, 0000)
|
|
if os.IsNotExist(err) {
|
|
if i == 0 {
|
|
// not even the first device was found -> fuse is not loaded
|
|
return nil, errNotLoaded
|
|
}
|
|
|
|
// we've run out of kernel-provided devices
|
|
return nil, errNoAvail
|
|
}
|
|
|
|
if err2, ok := err.(*os.PathError); ok && err2.Err == syscall.EBUSY {
|
|
// try the next one
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f, nil
|
|
}
|
|
}
|
|
|
|
func handleMountOSXFUSE(helperName string, errCh chan<- error) func(line string) (ignore bool) {
|
|
var noMountpointPrefix = helperName + `: `
|
|
const noMountpointSuffix = `: No such file or directory`
|
|
return func(line string) (ignore bool) {
|
|
if strings.HasPrefix(line, noMountpointPrefix) && strings.HasSuffix(line, noMountpointSuffix) {
|
|
// re-extract it from the error message in case some layer
|
|
// changed the path
|
|
mountpoint := line[len(noMountpointPrefix) : len(line)-len(noMountpointSuffix)]
|
|
err := &MountpointDoesNotExistError{
|
|
Path: mountpoint,
|
|
}
|
|
select {
|
|
case errCh <- err:
|
|
return true
|
|
default:
|
|
// not the first error; fall back to logging it
|
|
return false
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
// isBoringMountOSXFUSEError returns whether the Wait error is
|
|
// uninteresting; exit status 64 is.
|
|
func isBoringMountOSXFUSEError(err error) bool {
|
|
if err, ok := err.(*exec.ExitError); ok && err.Exited() {
|
|
if status, ok := err.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == 64 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func callMount(bin string, daemonVar string, dir string, conf *mountConfig, f *os.File, ready chan<- struct{}, errp *error) error {
|
|
for k, v := range conf.options {
|
|
if strings.Contains(k, ",") || strings.Contains(v, ",") {
|
|
// Silly limitation but the mount helper does not
|
|
// understand any escaping. See TestMountOptionCommaError.
|
|
return fmt.Errorf("mount options cannot contain commas on darwin: %q=%q", k, v)
|
|
}
|
|
}
|
|
cmd := exec.Command(
|
|
bin,
|
|
"-o", conf.getOptions(),
|
|
// 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(maxWrite, 10),
|
|
// refers to fd passed in cmd.ExtraFiles
|
|
"3",
|
|
dir,
|
|
)
|
|
cmd.ExtraFiles = []*os.File{f}
|
|
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)
|
|
}
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("setting up mount_osxfusefs stderr: %v", err)
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("setting up mount_osxfusefs stderr: %v", err)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("mount_osxfusefs: %v", err)
|
|
}
|
|
helperErrCh := make(chan error, 1)
|
|
go func() {
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go lineLogger(&wg, "mount helper output", neverIgnoreLine, stdout)
|
|
helperName := path.Base(bin)
|
|
go lineLogger(&wg, "mount helper error", handleMountOSXFUSE(helperName, helperErrCh), stderr)
|
|
wg.Wait()
|
|
if err := cmd.Wait(); err != nil {
|
|
// see if we have a better error to report
|
|
select {
|
|
case helperErr := <-helperErrCh:
|
|
// log the Wait error if it's not what we expected
|
|
if !isBoringMountOSXFUSEError(err) {
|
|
log.Printf("mount helper failed: %v", err)
|
|
}
|
|
// and now return what we grabbed from stderr as the real
|
|
// error
|
|
*errp = helperErr
|
|
close(ready)
|
|
return
|
|
default:
|
|
// nope, fall back to generic message
|
|
}
|
|
|
|
*errp = fmt.Errorf("mount_osxfusefs: %v", err)
|
|
close(ready)
|
|
return
|
|
}
|
|
|
|
*errp = nil
|
|
close(ready)
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func mount(dir string, conf *mountConfig, ready chan<- struct{}, errp *error) (*os.File, error) {
|
|
locations := conf.osxfuseLocations
|
|
if locations == nil {
|
|
locations = []OSXFUSEPaths{
|
|
OSXFUSELocationV3,
|
|
OSXFUSELocationV2,
|
|
}
|
|
}
|
|
for _, loc := range locations {
|
|
if _, err := os.Stat(loc.Mount); os.IsNotExist(err) {
|
|
// try the other locations
|
|
continue
|
|
}
|
|
|
|
f, err := openOSXFUSEDev(loc.DevicePrefix)
|
|
if err == errNotLoaded {
|
|
err = loadOSXFUSE(loc.Load)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// try again
|
|
f, err = openOSXFUSEDev(loc.DevicePrefix)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = callMount(loc.Mount, loc.DaemonVar, dir, conf, f, ready, errp)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
return f, nil
|
|
}
|
|
return nil, ErrOSXFUSENotFound
|
|
}
|