mountlib: correctly daemonize for compatibility with automount - #5593

This patch will:
- add --daemon-wait flag to control the time to wait for background mount
- remove dependency on sevlyar/go-daemon and implement backgrounding directly
- avoid setsid during backgrounding as it can result in race under Automount
- provide a fallback PATH to correctly run `fusermount` under systemd as it
  runs mount units without standard environment variables
- correctly handle ^C pressed while background process is being setting up
This commit is contained in:
Ivan Andreev 2021-08-18 14:07:09 +03:00
parent 8c10dee510
commit 8b8a943dd8
9 changed files with 192 additions and 74 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

11
fs/daemon_other.go Normal file
View File

@ -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
}

21
fs/daemon_unix.go Normal file
View File

@ -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
}

2
go.mod
View File

@ -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

5
go.sum
View File

@ -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=

View File

@ -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)
}

View File

@ -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
}