mirror of
https://github.com/rclone/rclone.git
synced 2024-11-28 19:34:55 +01:00
caf5dd9d5e
Before this change we waited until until the timeout to check the
daemon was alive.
Now we check it every 100ms like we do the mount status.
This also fixes compiling on all platforms which was broken by the
previous change
9bfbf2a4a
mount: fix macOS not noticing errors with --daemon
See: https://forum.rclone.org/t/rclone-mount-daemon-exits-successfully-even-when-mount-fails/43146
363 lines
12 KiB
Go
363 lines
12 KiB
Go
// Package mountlib provides the mount command.
|
|
package mountlib
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/cmd"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config"
|
|
"github.com/rclone/rclone/fs/config/flags"
|
|
"github.com/rclone/rclone/fs/rc"
|
|
"github.com/rclone/rclone/lib/atexit"
|
|
"github.com/rclone/rclone/lib/daemonize"
|
|
"github.com/rclone/rclone/vfs"
|
|
"github.com/rclone/rclone/vfs/vfscommon"
|
|
"github.com/rclone/rclone/vfs/vfsflags"
|
|
|
|
sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
//go:embed mount.md
|
|
var mountHelp string
|
|
|
|
// Options for creating the mount
|
|
type Options struct {
|
|
DebugFUSE bool
|
|
AllowNonEmpty bool
|
|
AllowRoot bool
|
|
AllowOther bool
|
|
DefaultPermissions bool
|
|
WritebackCache bool
|
|
Daemon bool
|
|
DaemonWait time.Duration // time to wait for ready mount from daemon, maximum on Linux or constant on macOS/BSD
|
|
MaxReadAhead fs.SizeSuffix
|
|
ExtraOptions []string
|
|
ExtraFlags []string
|
|
AttrTimeout time.Duration // how long the kernel caches attribute for
|
|
DeviceName string
|
|
VolumeName string
|
|
NoAppleDouble bool
|
|
NoAppleXattr bool
|
|
DaemonTimeout time.Duration // OSXFUSE only
|
|
AsyncRead bool
|
|
NetworkMode bool // Windows only
|
|
CaseInsensitive fs.Tristate
|
|
}
|
|
|
|
// DefaultOpt is the default values for creating the mount
|
|
var DefaultOpt = Options{
|
|
MaxReadAhead: 128 * 1024,
|
|
AttrTimeout: 1 * time.Second, // how long the kernel caches attribute for
|
|
NoAppleDouble: true, // use noappledouble by default
|
|
NoAppleXattr: false, // do not use noapplexattr by default
|
|
AsyncRead: true, // do async reads by default
|
|
}
|
|
|
|
type (
|
|
// UnmountFn is called to unmount the file system
|
|
UnmountFn func() error
|
|
// MountFn is called to mount the file system
|
|
MountFn func(VFS *vfs.VFS, mountpoint string, opt *Options) (<-chan error, func() error, error)
|
|
)
|
|
|
|
// MountPoint represents a mount with options and runtime state
|
|
type MountPoint struct {
|
|
MountPoint string
|
|
MountedOn time.Time
|
|
MountOpt Options
|
|
VFSOpt vfscommon.Options
|
|
Fs fs.Fs
|
|
VFS *vfs.VFS
|
|
MountFn MountFn
|
|
UnmountFn UnmountFn
|
|
ErrChan <-chan error
|
|
}
|
|
|
|
// NewMountPoint makes a new mounting structure
|
|
func NewMountPoint(mount MountFn, mountPoint string, f fs.Fs, mountOpt *Options, vfsOpt *vfscommon.Options) *MountPoint {
|
|
return &MountPoint{
|
|
MountFn: mount,
|
|
MountPoint: mountPoint,
|
|
Fs: f,
|
|
MountOpt: *mountOpt,
|
|
VFSOpt: *vfsOpt,
|
|
}
|
|
}
|
|
|
|
// Global constants
|
|
const (
|
|
MaxLeafSize = 1024 // don't pass file names longer than this
|
|
)
|
|
|
|
func init() {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
// DaemonTimeout defaults to non-zero for macOS
|
|
// (this is a macOS specific kernel option unrelated to DaemonWait)
|
|
DefaultOpt.DaemonTimeout = 10 * time.Minute
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
// Linux provides /proc/mounts to check mount status
|
|
// so --daemon-wait means *maximum* time to wait
|
|
DefaultOpt.DaemonWait = 60 * time.Second
|
|
case "darwin", "openbsd", "freebsd", "netbsd":
|
|
// On BSD we can't check mount status yet
|
|
// so --daemon-wait is just a *constant* delay
|
|
DefaultOpt.DaemonWait = 5 * time.Second
|
|
}
|
|
|
|
// Opt must be assigned in the init block to ensure changes really get in
|
|
Opt = DefaultOpt
|
|
}
|
|
|
|
// Opt contains options set by command line flags
|
|
var Opt Options
|
|
|
|
// AddFlags adds the non filing system specific flags to the command
|
|
func AddFlags(flagSet *pflag.FlagSet) {
|
|
rc.AddOption("mount", &Opt)
|
|
flags.BoolVarP(flagSet, &Opt.DebugFUSE, "debug-fuse", "", Opt.DebugFUSE, "Debug the FUSE internals - needs -v", "Mount")
|
|
flags.DurationVarP(flagSet, &Opt.AttrTimeout, "attr-timeout", "", Opt.AttrTimeout, "Time for which file/directory attributes are cached", "Mount")
|
|
flags.StringArrayVarP(flagSet, &Opt.ExtraOptions, "option", "o", []string{}, "Option for libfuse/WinFsp (repeat if required)", "Mount")
|
|
flags.StringArrayVarP(flagSet, &Opt.ExtraFlags, "fuse-flag", "", []string{}, "Flags or arguments to be passed direct to libfuse/WinFsp (repeat if required)", "Mount")
|
|
// Non-Windows only
|
|
flags.BoolVarP(flagSet, &Opt.Daemon, "daemon", "", Opt.Daemon, "Run mount in background and exit parent process (as background output is suppressed, use --log-file with --log-format=pid,... to monitor) (not supported on Windows)", "Mount")
|
|
flags.DurationVarP(flagSet, &Opt.DaemonTimeout, "daemon-timeout", "", Opt.DaemonTimeout, "Time limit for rclone to respond to kernel (not supported on Windows)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.DefaultPermissions, "default-permissions", "", Opt.DefaultPermissions, "Makes kernel enforce access control based on the file mode (not supported on Windows)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.AllowNonEmpty, "allow-non-empty", "", Opt.AllowNonEmpty, "Allow mounting over a non-empty directory (not supported on Windows)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.AllowRoot, "allow-root", "", Opt.AllowRoot, "Allow access to root user (not supported on Windows)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.AllowOther, "allow-other", "", Opt.AllowOther, "Allow access to other users (not supported on Windows)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.AsyncRead, "async-read", "", Opt.AsyncRead, "Use asynchronous reads (not supported on Windows)", "Mount")
|
|
flags.FVarP(flagSet, &Opt.MaxReadAhead, "max-read-ahead", "", "The number of bytes that can be prefetched for sequential reads (not supported on Windows)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.WritebackCache, "write-back-cache", "", Opt.WritebackCache, "Makes kernel buffer writes before sending them to rclone (without this, writethrough caching is used) (not supported on Windows)", "Mount")
|
|
flags.StringVarP(flagSet, &Opt.DeviceName, "devname", "", Opt.DeviceName, "Set the device name - default is remote:path", "Mount")
|
|
flags.FVarP(flagSet, &Opt.CaseInsensitive, "mount-case-insensitive", "", "Tell the OS the mount is case insensitive (true) or sensitive (false) regardless of the backend (auto)", "Mount")
|
|
// Windows and OSX
|
|
flags.StringVarP(flagSet, &Opt.VolumeName, "volname", "", Opt.VolumeName, "Set the volume name (supported on Windows and OSX only)", "Mount")
|
|
// OSX only
|
|
flags.BoolVarP(flagSet, &Opt.NoAppleDouble, "noappledouble", "", Opt.NoAppleDouble, "Ignore Apple Double (._) and .DS_Store files (supported on OSX only)", "Mount")
|
|
flags.BoolVarP(flagSet, &Opt.NoAppleXattr, "noapplexattr", "", Opt.NoAppleXattr, "Ignore all \"com.apple.*\" extended attributes (supported on OSX only)", "Mount")
|
|
// Windows only
|
|
flags.BoolVarP(flagSet, &Opt.NetworkMode, "network-mode", "", Opt.NetworkMode, "Mount as remote network drive, instead of fixed disk drive (supported on Windows only)", "Mount")
|
|
// Unix only
|
|
flags.DurationVarP(flagSet, &Opt.DaemonWait, "daemon-wait", "", Opt.DaemonWait, "Time to wait for ready mount from daemon (maximum time on Linux, constant sleep time on OSX/BSD) (not supported on Windows)", "Mount")
|
|
}
|
|
|
|
const (
|
|
pollInterval = 100 * time.Millisecond
|
|
)
|
|
|
|
// WaitMountReady waits until mountpoint is mounted by rclone.
|
|
//
|
|
// If the mount daemon dies prematurely it will notice too.
|
|
func WaitMountReady(mountpoint string, timeout time.Duration, daemon *os.Process) (err error) {
|
|
endTime := time.Now().Add(timeout)
|
|
for {
|
|
if CanCheckMountReady {
|
|
err = CheckMountReady(mountpoint)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
err = daemonize.Check(daemon)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delay := time.Until(endTime)
|
|
if delay <= 0 {
|
|
break
|
|
}
|
|
if delay > pollInterval {
|
|
delay = pollInterval
|
|
}
|
|
time.Sleep(delay)
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewMountCommand makes a mount command with the given name and Mount function
|
|
func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Command {
|
|
var commandDefinition = &cobra.Command{
|
|
Use: commandName + " remote:path /path/to/mountpoint",
|
|
Hidden: hidden,
|
|
Short: `Mount the remote as file system on a mountpoint.`,
|
|
Long: strings.ReplaceAll(mountHelp, "@", commandName) + vfs.Help,
|
|
Annotations: map[string]string{
|
|
"versionIntroduced": "v1.33",
|
|
"groups": "Filter",
|
|
},
|
|
Run: func(command *cobra.Command, args []string) {
|
|
cmd.CheckArgs(2, 2, command, args)
|
|
|
|
if fs.GetConfig(context.Background()).UseListR {
|
|
fs.Logf(nil, "--fast-list does nothing on a mount")
|
|
}
|
|
|
|
if Opt.Daemon {
|
|
config.PassConfigKeyForDaemonization = true
|
|
}
|
|
|
|
if os.Getenv("PATH") == "" && runtime.GOOS != "windows" {
|
|
// PATH can be unset when running under Autofs or Systemd mount
|
|
fs.Debugf(nil, "Using fallback PATH to run fusermount")
|
|
_ = os.Setenv("PATH", "/bin:/usr/bin")
|
|
}
|
|
|
|
// Show stats if the user has specifically requested them
|
|
if cmd.ShowStats() {
|
|
defer cmd.StartStats()()
|
|
}
|
|
|
|
mnt := NewMountPoint(mount, args[1], cmd.NewFsDir(args), &Opt, &vfsflags.Opt)
|
|
daemon, err := mnt.Mount()
|
|
|
|
// Wait for foreground mount, if any...
|
|
if daemon == nil {
|
|
if err == nil {
|
|
err = mnt.Wait()
|
|
}
|
|
if err != nil {
|
|
log.Fatalf("Fatal error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Wait for daemon, if any...
|
|
killOnce := sync.Once{}
|
|
killDaemon := func(reason string) {
|
|
killOnce.Do(func() {
|
|
if err := daemon.Signal(os.Interrupt); err != nil {
|
|
fs.Errorf(nil, "%s. Failed to terminate daemon pid %d: %v", reason, daemon.Pid, err)
|
|
return
|
|
}
|
|
fs.Debugf(nil, "%s. Terminating daemon pid %d", reason, daemon.Pid)
|
|
})
|
|
}
|
|
|
|
if err == nil && Opt.DaemonWait > 0 {
|
|
handle := atexit.Register(func() {
|
|
killDaemon("Got interrupt")
|
|
})
|
|
err = WaitMountReady(mnt.MountPoint, Opt.DaemonWait, daemon)
|
|
if err != nil {
|
|
killDaemon("Daemon timed out")
|
|
}
|
|
atexit.Unregister(handle)
|
|
}
|
|
if err != nil {
|
|
log.Fatalf("Fatal error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
// Register the command
|
|
cmd.Root.AddCommand(commandDefinition)
|
|
|
|
// Add flags
|
|
cmdFlags := commandDefinition.Flags()
|
|
AddFlags(cmdFlags)
|
|
vfsflags.AddFlags(cmdFlags)
|
|
|
|
return commandDefinition
|
|
}
|
|
|
|
// Mount the remote at mountpoint
|
|
func (m *MountPoint) Mount() (daemon *os.Process, err error) {
|
|
|
|
// Ensure sensible defaults
|
|
m.SetVolumeName(m.MountOpt.VolumeName)
|
|
m.SetDeviceName(m.MountOpt.DeviceName)
|
|
|
|
// Start background task if --daemon is specified
|
|
if m.MountOpt.Daemon {
|
|
daemon, err = daemonize.StartDaemon(os.Args)
|
|
if daemon != nil || err != nil {
|
|
return daemon, err
|
|
}
|
|
}
|
|
|
|
m.VFS = vfs.New(m.Fs, &m.VFSOpt)
|
|
|
|
m.ErrChan, m.UnmountFn, err = m.MountFn(m.VFS, m.MountPoint, &m.MountOpt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to mount FUSE fs: %w", err)
|
|
}
|
|
m.MountedOn = time.Now()
|
|
return nil, nil
|
|
}
|
|
|
|
// Wait for mount end
|
|
func (m *MountPoint) Wait() error {
|
|
// Unmount on exit
|
|
var finaliseOnce sync.Once
|
|
finalise := func() {
|
|
finaliseOnce.Do(func() {
|
|
_ = sysdnotify.Stopping()
|
|
// Unmount only if directory was mounted by rclone, e.g. don't unmount autofs hooks.
|
|
if err := CheckMountReady(m.MountPoint); err != nil {
|
|
fs.Debugf(m.MountPoint, "Unmounted externally. Just exit now.")
|
|
return
|
|
}
|
|
if err := m.Unmount(); err != nil {
|
|
fs.Errorf(m.MountPoint, "Failed to unmount: %v", err)
|
|
} else {
|
|
fs.Errorf(m.MountPoint, "Unmounted rclone mount")
|
|
}
|
|
})
|
|
}
|
|
fnHandle := atexit.Register(finalise)
|
|
defer atexit.Unregister(fnHandle)
|
|
|
|
// Notify systemd
|
|
if err := sysdnotify.Ready(); err != nil {
|
|
return fmt.Errorf("failed to notify systemd: %w", err)
|
|
}
|
|
|
|
// Reload VFS cache on SIGHUP
|
|
sigHup := make(chan os.Signal, 1)
|
|
NotifyOnSigHup(sigHup)
|
|
var err error
|
|
|
|
waiting := true
|
|
for waiting {
|
|
select {
|
|
// umount triggered outside the app
|
|
case err = <-m.ErrChan:
|
|
waiting = false
|
|
// user sent SIGHUP to clear the cache
|
|
case <-sigHup:
|
|
root, err := m.VFS.Root()
|
|
if err != nil {
|
|
fs.Errorf(m.VFS.Fs(), "Error reading root: %v", err)
|
|
} else {
|
|
root.ForgetAll()
|
|
}
|
|
}
|
|
}
|
|
|
|
finalise()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to umount FUSE fs: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unmount the specified mountpoint
|
|
func (m *MountPoint) Unmount() (err error) {
|
|
return m.UnmountFn()
|
|
}
|