diff --git a/go/kbfs/libfuse/file.go b/go/kbfs/libfuse/file.go index 9a6667c31532..e76286b34f7e 100644 --- a/go/kbfs/libfuse/file.go +++ b/go/kbfs/libfuse/file.go @@ -192,6 +192,12 @@ func (f *File) sync(ctx context.Context) error { // Fsync implements the fs.NodeFsyncer interface for File. func (f *File) Fsync(ctx context.Context, req *fuse.FsyncRequest) (err error) { + ctx, maybeUnmounting, cancel := wrapCtxWithShorterTimeoutForUnmount(f.folder.fs.log, ctx, int(req.Pid)) + defer cancel() + if maybeUnmounting { + f.folder.fs.log.CInfof(ctx, "Fsync: maybeUnmounting=true") + } + ctx = f.folder.fs.config.MaybeStartTrace( ctx, "File.Fsync", f.node.GetBasename().String()) defer func() { f.folder.fs.config.MaybeFinishTrace(ctx, err) }() @@ -199,11 +205,14 @@ func (f *File) Fsync(ctx context.Context, req *fuse.FsyncRequest) (err error) { f.folder.fs.vlog.CLogf(ctx, libkb.VLog1, "File Fsync") defer func() { err = f.folder.processError(ctx, libkbfs.WriteMode, err) }() - // This fits in situation 1 as described in libkbfs/delayed_cancellation.go - err = libcontext.EnableDelayedCancellationWithGracePeriod( - ctx, f.folder.fs.config.DelayedCancellationGracePeriod()) - if err != nil { - return err + if !maybeUnmounting { + // This fits in situation 1 as described in + // libkbfs/delayed_cancellation.go + err = libcontext.EnableDelayedCancellationWithGracePeriod( + ctx, f.folder.fs.config.DelayedCancellationGracePeriod()) + if err != nil { + return err + } } return f.sync(ctx) diff --git a/go/kbfs/libfuse/fs.go b/go/kbfs/libfuse/fs.go index 45107448d13f..5a087ca071d3 100644 --- a/go/kbfs/libfuse/fs.go +++ b/go/kbfs/libfuse/fs.go @@ -381,13 +381,14 @@ func (f *FS) Root() (fs.Node, error) { return f.root, nil } -// quotaUsageStaleTolerance is the lifespan of stale usage data that libfuse -// accepts in the Statfs handler. In other words, this causes libkbfs to issue -// a fresh RPC call if cached usage data is older than 10s. -const quotaUsageStaleTolerance = 10 * time.Second - // Statfs implements the fs.FSStatfser interface for FS. func (f *FS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *fuse.StatfsResponse) error { + ctx, maybeUnmounting, cancel := wrapCtxWithShorterTimeoutForUnmount(f.log, ctx, int(req.Pid)) + defer cancel() + if maybeUnmounting { + f.log.CInfof(ctx, "Statfs: maybeUnmounting=true") + } + *resp = fuse.StatfsResponse{ Bsize: fuseBlockSize, Namelen: ^uint32(0), diff --git a/go/kbfs/libfuse/mounter_non_osx.go b/go/kbfs/libfuse/mounter_non_osx.go index 00fce1d94f4a..305527cd5a47 100644 --- a/go/kbfs/libfuse/mounter_non_osx.go +++ b/go/kbfs/libfuse/mounter_non_osx.go @@ -6,7 +6,12 @@ package libfuse -import "bazil.org/fuse" +import ( + "context" + + "bazil.org/fuse" + "github.com/keybase/client/go/logger" +) func getPlatformSpecificMountOptions(dir string, platformParams PlatformParams) ([]fuse.MountOption, error) { options := []fuse.MountOption{} @@ -27,3 +32,11 @@ func translatePlatformSpecificError(err error, platformParams PlatformParams) er func (m *mounter) reinstallMountDirIfPossible() { // no-op } + +var noop = func() {} + +func wrapCtxWithShorterTimeoutForUnmount( + _ logger.Logger, ctx context.Context, _ int) ( + newCtx context.Context, maybeUnmounting bool, cancel context.CancelFunc) { + return ctx, false, noop +} diff --git a/go/kbfs/libfuse/mounter_osx.go b/go/kbfs/libfuse/mounter_osx.go index 25e0ab29fe5d..81aace46d4bd 100644 --- a/go/kbfs/libfuse/mounter_osx.go +++ b/go/kbfs/libfuse/mounter_osx.go @@ -7,10 +7,13 @@ package libfuse import ( + "context" "errors" + "time" "bazil.org/fuse" "github.com/keybase/client/go/install/libnativeinstaller" + "github.com/keybase/client/go/logger" ) var kbfusePath = fuse.OSXFUSEPaths{ @@ -84,3 +87,39 @@ func (m *mounter) reinstallMountDirIfPossible() { err = libnativeinstaller.InstallMountDir(m.runMode, m.log) m.log.Debug("InstallMountDir: err=%v", err) } + +// quotaUsageStaleTolerance is the lifespan of stale usage data that libfuse +// accepts in the Statfs handler. In other words, this causes libkbfs to issue +// a fresh RPC call if cached usage data is older than 10s. +const quotaUsageStaleTolerance = 10 * time.Second + +const unmountCallTolerance = time.Second + +var unmountingExecPaths = map[string]bool{ + "/usr/sbin/diskutil": true, + "/usr/libexec/lsd": true, + "/sbin/umount": true, +} + +var noop = func() {} + +// wrapCtxWithShorterTimeoutForUnmount wraps ctx witha a timeout of +// unmountCallTolerance if pid is /usr/sbin/diskutil, /usr/libexec/lsd, or +// /sbin/umount. This is useful for calls that usually happen during unmounting +// such as Statfs and Fsync. If we block on those calls, `diskutil umount force +// ` is blocked as well. So make them timeout after 2s to make unmounting +// work. +func wrapCtxWithShorterTimeoutForUnmount( + log logger.Logger, ctx context.Context, pid int) ( + newCtx context.Context, maybeUnmounting bool, cancel context.CancelFunc) { + p, err := pidPath(pid) + if err != nil { + return ctx, false, noop + } + if unmountingExecPaths[p] { + log.CDebugf(ctx, "wrapping context with timeout for %s", p) + newCtx, cancel = context.WithTimeout(ctx, unmountCallTolerance) + return newCtx, true, cancel + } + return ctx, false, noop +} diff --git a/go/kbfs/libfuse/pidpath_darwin.go b/go/kbfs/libfuse/pidpath_darwin.go new file mode 100644 index 000000000000..5fca3047de6c --- /dev/null +++ b/go/kbfs/libfuse/pidpath_darwin.go @@ -0,0 +1,41 @@ +// Copyright 2021 Keybase Inc. All rights reserved. +// Use of this source code is governed by a BSD +// license that can be found in the LICENSE file. +// +// +build darwin + +package libfuse + +// #include +// #include +// #include +import "C" + +import ( + "errors" + "strconv" + "unsafe" +) + +// pidPath returns the exec path for process pid. Adapted from +// https://ops.tips/blog/macos-pid-absolute-path-and-procfs-exploration/ +func pidPath(pid int) (path string, err error) { + const bufSize = C.PROC_PIDPATHINFO_MAXSIZE + buf := C.CString(string(make([]byte, bufSize))) + defer C.free(unsafe.Pointer(buf)) + + ret, err := C.proc_pidpath(C.int(pid), unsafe.Pointer(buf), bufSize) + if err != nil { + return "", err + } + if ret < 0 { + return "", errors.New( + "error calling proc_pidpath. exit code: " + strconv.Itoa(int(ret))) + } + if ret == 0 { + return "", errors.New("proc_pidpath returned empty buffer") + } + + path = C.GoString(buf) + return +} diff --git a/go/kbfs/libfuse/pidpath_others.go b/go/kbfs/libfuse/pidpath_others.go new file mode 100644 index 000000000000..0dc96c69040b --- /dev/null +++ b/go/kbfs/libfuse/pidpath_others.go @@ -0,0 +1,15 @@ +// Copyright 2021 Keybase Inc. All rights reserved. +// Use of this source code is governed by a BSD +// license that can be found in the LICENSE file. +// +// +build !darwin + +package libfuse + +import "github.com/pkg/errors" + +var notImplementedErr = errors.New("unimplemented") + +func pidPath(_ int) (path string, err error) { + return "", notImplementedErr +} diff --git a/go/mounter/mounter_non_osx.go b/go/mounter/mounter_non_osx.go index fe48ebb89277..77f37e17d22d 100644 --- a/go/mounter/mounter_non_osx.go +++ b/go/mounter/mounter_non_osx.go @@ -5,7 +5,9 @@ package mounter -import "fmt" +import ( + "fmt" +) // IsMounted returns true if directory is mounted (by kbfuse) func IsMounted(dir string, log Log) (bool, error) {