github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libfuse/mounter_osx.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  //
     5  //go:build darwin
     6  // +build darwin
     7  
     8  package libfuse
     9  
    10  // #include <libproc.h>
    11  // #include <stdlib.h>
    12  // #include <errno.h>
    13  import "C"
    14  
    15  import (
    16  	"context"
    17  	"errors"
    18  	"strconv"
    19  	"time"
    20  	"unsafe"
    21  
    22  	"bazil.org/fuse"
    23  	"github.com/keybase/client/go/install/libnativeinstaller"
    24  	"github.com/keybase/client/go/logger"
    25  )
    26  
    27  var kbfusePath = fuse.OSXFUSEPaths{
    28  	DevicePrefix: "/dev/kbfuse",
    29  	Load:         "/Library/Filesystems/kbfuse.fs/Contents/Resources/load_kbfuse",
    30  	Mount:        "/Library/Filesystems/kbfuse.fs/Contents/Resources/mount_kbfuse",
    31  	DaemonVar:    "_FUSE_DAEMON_PATH",
    32  }
    33  
    34  func getPlatformSpecificMountOptions(dir string, platformParams PlatformParams) ([]fuse.MountOption, error) {
    35  	options := []fuse.MountOption{}
    36  
    37  	var locationOption fuse.MountOption
    38  	if platformParams.UseSystemFuse {
    39  		// Only allow osxfuse 3.x.
    40  		locationOption = fuse.OSXFUSELocations(fuse.OSXFUSELocationV3)
    41  	} else {
    42  		// Only allow kbfuse.
    43  		locationOption = fuse.OSXFUSELocations(kbfusePath)
    44  	}
    45  	options = append(options, locationOption)
    46  
    47  	// Volume name option is only used on OSX (ignored on other platforms).
    48  	volName, err := volumeName(dir)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	options = append(options, fuse.VolumeName(volName))
    54  	options = append(options, fuse.ExclCreate())
    55  
    56  	if platformParams.UseLocal {
    57  		options = append(options, fuse.LocalVolume())
    58  	}
    59  
    60  	return options, nil
    61  }
    62  
    63  // GetPlatformSpecificMountOptionsForTest makes cross-platform tests work
    64  func GetPlatformSpecificMountOptionsForTest() []fuse.MountOption {
    65  	// For now, test with either kbfuse or OSXFUSE for now.
    66  	// TODO: Consider mandate testing with kbfuse?
    67  	return []fuse.MountOption{
    68  		fuse.OSXFUSELocations(kbfusePath, fuse.OSXFUSELocationV3),
    69  		fuse.ExclCreate(),
    70  
    71  		// We are diabling the 'local' mount option in tests for now since it
    72  		// causes out tests to leave a bunch of entries behind in Finder's sidebar.
    73  		// TODO: fix this.
    74  		// fuse.LocalVolume(),
    75  	}
    76  }
    77  
    78  func translatePlatformSpecificError(err error, platformParams PlatformParams) error {
    79  	if err == fuse.ErrOSXFUSENotFound {
    80  		if platformParams.UseSystemFuse {
    81  			return errors.New(
    82  				"cannot locate OSXFUSE 3.x")
    83  		}
    84  		return errors.New(
    85  			"cannot locate kbfuse; either install the Keybase " +
    86  				"app, or install OSXFUSE 3.x " +
    87  				"and pass in --use-system-fuse")
    88  	}
    89  	return err
    90  }
    91  
    92  func (m *mounter) reinstallMountDirIfPossible() {
    93  	err := libnativeinstaller.UninstallMountDir(m.runMode, m.log)
    94  	m.log.Debug("UninstallMountDir: err=%v", err)
    95  	err = libnativeinstaller.InstallMountDir(m.runMode, m.log)
    96  	m.log.Debug("InstallMountDir: err=%v", err)
    97  }
    98  
    99  const unmountCallTolerance = time.Second
   100  
   101  var unmountingExecPaths = map[string]bool{
   102  	"/usr/sbin/diskutil": true,
   103  	"/usr/libexec/lsd":   true,
   104  	"/sbin/umount":       true,
   105  }
   106  
   107  var noop = func() {}
   108  
   109  // pidPath returns the exec path for process pid. Adapted from
   110  // https://ops.tips/blog/macos-pid-absolute-path-and-procfs-exploration/
   111  func pidPath(pid int) (path string, err error) {
   112  	const bufSize = C.PROC_PIDPATHINFO_MAXSIZE
   113  	buf := C.CString(string(make([]byte, bufSize)))
   114  	defer C.free(unsafe.Pointer(buf))
   115  
   116  	ret, err := C.proc_pidpath(C.int(pid), unsafe.Pointer(buf), bufSize)
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  	if ret < 0 {
   121  		return "", errors.New(
   122  			"error calling proc_pidpath. exit code: " + strconv.Itoa(int(ret)))
   123  	}
   124  	if ret == 0 {
   125  		return "", errors.New("proc_pidpath returned empty buffer")
   126  	}
   127  
   128  	path = C.GoString(buf)
   129  	return
   130  }
   131  
   132  // wrapCtxWithShorterTimeoutForUnmount wraps ctx witha a timeout of
   133  // unmountCallTolerance if pid is /usr/sbin/diskutil, /usr/libexec/lsd, or
   134  // /sbin/umount. This is useful for calls that usually happen during unmounting
   135  // such as Statfs and Fsync. If we block on those calls, `diskutil umount force
   136  // <mnt>` is blocked as well. So make them timeout after 2s to make unmounting
   137  // work.
   138  func wrapCtxWithShorterTimeoutForUnmount(ctx context.Context,
   139  	log logger.Logger, pid int) (
   140  	newCtx context.Context, maybeUnmounting bool, cancel context.CancelFunc) {
   141  	p, err := pidPath(pid)
   142  	if err != nil {
   143  		return ctx, false, noop
   144  	}
   145  	if unmountingExecPaths[p] {
   146  		log.CDebugf(ctx, "wrapping context with timeout for %s", p)
   147  		newCtx, cancel = context.WithTimeout(ctx, unmountCallTolerance)
   148  		return newCtx, true, cancel
   149  	}
   150  	return ctx, false, noop
   151  }