diff --git a/cmd/mountlib/daemon.go b/cmd/mountlib/daemon.go deleted file mode 100644 index 1896afef2..000000000 --- a/cmd/mountlib/daemon.go +++ /dev/null @@ -1,16 +0,0 @@ -// Daemonization interface for non-Unix variants only - -//go:build windows || plan9 || js -// +build windows plan9 js - -package mountlib - -import ( - "log" - "runtime" -) - -func startBackgroundMode() bool { - log.Fatalf("background mode not supported on %s platform", runtime.GOOS) - return false -} diff --git a/cmd/mountlib/daemon_unix.go b/cmd/mountlib/daemon_unix.go deleted file mode 100644 index 7aa76c3ca..000000000 --- a/cmd/mountlib/daemon_unix.go +++ /dev/null @@ -1,32 +0,0 @@ -// Daemonization interface for Unix variants only - -//go:build !windows && !plan9 && !js -// +build !windows,!plan9,!js - -package mountlib - -import ( - "log" - - daemon "github.com/sevlyar/go-daemon" -) - -func startBackgroundMode() bool { - cntxt := &daemon.Context{} - d, err := cntxt.Reborn() - if err != nil { - log.Fatalln(err) - } - - if d != nil { - return true - } - - defer func() { - if err := cntxt.Release(); err != nil { - log.Printf("error encountered while killing daemon: %v", err) - } - }() - - return false -} diff --git a/cmd/mountlib/mount.go b/cmd/mountlib/mount.go index f85a4ea20..c3581f7b8 100644 --- a/cmd/mountlib/mount.go +++ b/cmd/mountlib/mount.go @@ -15,6 +15,7 @@ import ( "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" @@ -34,6 +35,7 @@ type Options struct { 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 @@ -81,16 +83,30 @@ const ( ) func init() { - // DaemonTimeout defaults to non zero for macOS - if runtime.GOOS == "darwin" { + 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 } -// Options set by command line flags -var ( - 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) { @@ -100,7 +116,7 @@ func AddFlags(flagSet *pflag.FlagSet) { flags.StringArrayVarP(flagSet, &Opt.ExtraOptions, "option", "o", []string{}, "Option for libfuse/WinFsp. Repeat if required.") flags.StringArrayVarP(flagSet, &Opt.ExtraFlags, "fuse-flag", "", []string{}, "Flags or arguments to be passed direct to libfuse/WinFsp. Repeat if required.") // Non-Windows only - flags.BoolVarP(flagSet, &Opt.Daemon, "daemon", "", Opt.Daemon, "Run mount as a daemon (background mode). Not supported on Windows.") + flags.BoolVarP(flagSet, &Opt.Daemon, "daemon", "", Opt.Daemon, "Run mount in background and exit parent process. Not supported on Windows. As background output is suppressed, use --log-file with --log-format=pid,... to monitor.") flags.DurationVarP(flagSet, &Opt.DaemonTimeout, "daemon-timeout", "", Opt.DaemonTimeout, "Time limit for rclone to respond to kernel. Not supported on Windows.") flags.BoolVarP(flagSet, &Opt.DefaultPermissions, "default-permissions", "", Opt.DefaultPermissions, "Makes kernel enforce access control based on the file mode. Not supported on Windows.") flags.BoolVarP(flagSet, &Opt.AllowNonEmpty, "allow-non-empty", "", Opt.AllowNonEmpty, "Allow mounting over a non-empty directory. Not supported on Windows.") @@ -116,6 +132,8 @@ func AddFlags(flagSet *pflag.FlagSet) { flags.BoolVarP(flagSet, &Opt.NoAppleXattr, "noapplexattr", "", Opt.NoAppleXattr, "Ignore all \"com.apple.*\" extended attributes. Supported on OSX only.") // 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") + // 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). Ignored on Windows.") } // NewMountCommand makes a mount command with the given name and Mount function @@ -136,6 +154,12 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm 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()() @@ -149,9 +173,40 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm VFSOpt: vfsflags.Opt, } - daemonized, err := mnt.Mount() - if !daemonized && err == nil { - err = mnt.Wait() + 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) + if err != nil { + killDaemon("Daemon timed out") + } + atexit.Unregister(handle) } if err != nil { log.Fatalf("Fatal error: %v", err) @@ -171,21 +226,21 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm } // Mount the remote at mountpoint -func (m *MountPoint) Mount() (daemonized bool, err error) { +func (m *MountPoint) Mount() (daemon *os.Process, err error) { if err = m.CheckOverlap(); err != nil { - return false, err + return nil, err } if err = m.CheckAllowings(); err != nil { - return false, err + return nil, err } m.SetVolumeName(m.MountOpt.VolumeName) // Start background task if --daemon is specified if m.MountOpt.Daemon { - daemonized = startBackgroundMode() - if daemonized { - return true, nil + daemon, err = daemonize.StartDaemon(os.Args) + if daemon != nil || err != nil { + return daemon, err } } @@ -193,9 +248,9 @@ func (m *MountPoint) Mount() (daemonized bool, err error) { m.ErrChan, m.UnmountFn, err = m.MountFn(m.VFS, m.MountPoint, &m.MountOpt) if err != nil { - return false, errors.Wrap(err, "failed to mount FUSE fs") + return nil, errors.Wrap(err, "failed to mount FUSE fs") } - return false, nil + return nil, nil } // Wait for mount end @@ -205,7 +260,16 @@ func (m *MountPoint) Wait() error { finalise := func() { finaliseOnce.Do(func() { _ = sysdnotify.Stopping() - _ = m.UnmountFn() + // 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) diff --git a/fs/daemon_other.go b/fs/daemon_other.go new file mode 100644 index 000000000..c012ba735 --- /dev/null +++ b/fs/daemon_other.go @@ -0,0 +1,11 @@ +// Daemonization stub for non-Unix platforms (common definitions) + +//go:build windows || plan9 || js +// +build windows plan9 js + +package fs + +// IsDaemon returns true if this process runs in background +func IsDaemon() bool { + return false +} diff --git a/fs/daemon_unix.go b/fs/daemon_unix.go new file mode 100644 index 000000000..8951cbbc7 --- /dev/null +++ b/fs/daemon_unix.go @@ -0,0 +1,21 @@ +// Daemonization interface for Unix platforms (common definitions) + +//go:build !windows && !plan9 && !js +// +build !windows,!plan9,!js + +package fs + +import ( + "os" +) + +// We use a special environment variable to let the child process know its role. +const ( + DaemonMarkVar = "_RCLONE_DAEMON_" + DaemonMarkChild = "_rclone_daemon_" +) + +// IsDaemon returns true if this process runs in background +func IsDaemon() bool { + return os.Getenv(DaemonMarkVar) == DaemonMarkChild +} diff --git a/go.mod b/go.mod index 842c2d3f7..b4717fd70 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/jcmturner/gokrb5/v8 v8.4.2 github.com/jlaffaye/ftp v0.0.0-20210307004419-5d4190119067 github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 - github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/klauspost/compress v1.13.4 github.com/koofr/go-httpclient v0.0.0-20200420163713-93aa7c75b348 github.com/koofr/go-koofrclient v0.0.0-20190724113126-8e5366da203a @@ -56,7 +55,6 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8 github.com/rfjakob/eme v1.1.2 - github.com/sevlyar/go-daemon v0.1.5 github.com/shirou/gopsutil/v3 v3.21.8 github.com/sirupsen/logrus v1.8.1 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 diff --git a/go.sum b/go.sum index 4c47cc09b..e578fecc2 100644 --- a/go.sum +++ b/go.sum @@ -391,8 +391,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg= github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= @@ -548,8 +546,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk= -github.com/sevlyar/go-daemon v0.1.5/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA= github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= @@ -1156,7 +1152,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/daemonize/daemon_other.go b/lib/daemonize/daemon_other.go new file mode 100644 index 000000000..70e4a6df5 --- /dev/null +++ b/lib/daemonize/daemon_other.go @@ -0,0 +1,18 @@ +// Daemonization stub for non-Unix platforms (implementation) + +//go:build windows || plan9 || js +// +build windows plan9 js + +package daemonize + +import ( + "os" + "runtime" + + "github.com/pkg/errors" +) + +// StartDaemon runs background twin of current process. +func StartDaemon(args []string) (*os.Process, error) { + return nil, errors.Errorf("background mode is not supported on %s platform", runtime.GOOS) +} diff --git a/lib/daemonize/daemon_unix.go b/lib/daemonize/daemon_unix.go new file mode 100644 index 000000000..325ee3f80 --- /dev/null +++ b/lib/daemonize/daemon_unix.go @@ -0,0 +1,59 @@ +// Daemonization interface for Unix platforms (implementation) + +//go:build !windows && !plan9 && !js +// +build !windows,!plan9,!js + +package daemonize + +import ( + "os" + "syscall" + + "github.com/rclone/rclone/fs" +) + +// StartDaemon runs background twin of current process. +// It executes separate parts of code in child and parent processes. +// Returns child process pid in the parent or nil in the child. +// The method looks like a fork but safe for goroutines. +func StartDaemon(args []string) (*os.Process, error) { + if fs.IsDaemon() { + // This process is already daemonized + return nil, nil + } + + env := append(os.Environ(), fs.DaemonMarkVar+"="+fs.DaemonMarkChild) + + me, err := os.Executable() + if err != nil { + me = os.Args[0] + } + + null, err := os.Open(os.DevNull) + if err != nil { + return nil, err + } + files := []*os.File{ + null, // (0) stdin + null, // (1) stdout + null, // (2) stderr + } + sysAttr := &syscall.SysProcAttr{ + // setsid (https://linux.die.net/man/2/setsid) in the child process will reset + // its process group id (PGID) to its PID thus detaching it from parent. + // This would make autofs fail because it detects mounting process by its PGID. + Setsid: false, + } + attr := &os.ProcAttr{ + Env: env, + Files: files, + Sys: sysAttr, + } + + daemon, err := os.StartProcess(me, args, attr) + if err != nil { + return nil, err + } + + return daemon, nil +}