From b11af30cb11538677a1df79fc0d79d330a91e39b Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 11 Nov 2026 10:11:58 -0800 Subject: [PATCH] mount: remove the local control socket when the mount exits (#12560) * mount: remove the local control socket when the mount exits weed mount listened on /tmp/seaweedfs-mount-.sock and removed it only when a later mount of the same directory started, so every unmount or SIGTERM left one socket file behind. Remove it on the normal return path and in the interrupt hook, unless a later mount of the same directory has already replaced it. Co-Authored-By: Claude Opus 5.5 * mount: remove the control socket before a failed mount exits glog.Fatalf and glog.Exitf call os.Exit, which skips the deferred removal, so a mount that failed after creating its socket (mount point not available, fusermount failure) still left it behind. Co-Authored-By: Claude Opus 5.5 * mount: register the socket cleanup hook before mount setup A SIGTERM that arrived while the mount was still setting up, for example waiting on the filer to create the mount root, exited through grace without running the deferred cleanup. Registering the hook right after the listener is created makes it run last, after the unmount and filesystem hooks. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- weed/command/mount_common.go | 22 ++++++++++++++++ weed/command/mount_common_test.go | 42 +++++++++++++++++++++++++++++++ weed/command/mount_std.go | 6 +++++ weed/command/mount_windows.go | 3 +++ 4 files changed, 73 insertions(+) diff --git a/weed/command/mount_common.go b/weed/command/mount_common.go index bbabe56ac8..5321c1af78 100644 --- a/weed/command/mount_common.go +++ b/weed/command/mount_common.go @@ -14,6 +14,7 @@ import ( "runtime/debug" "strconv" "strings" + "sync" "time" "google.golang.org/grpc" @@ -269,6 +270,27 @@ func createMountRoot(wfs *mount.WFS, mountRoot, bucketRootPath string, filerAddr return true } +// mountSocketCleanup returns a func that removes the control socket this mount +// listens on, unless a later mount of the same directory has replaced it. +func mountSocketCleanup(socketPath string) func() { + listened, err := os.Lstat(socketPath) + if err != nil { + return func() {} + } + var once sync.Once + return func() { + once.Do(func() { + current, err := os.Lstat(socketPath) + if err != nil || !os.SameFile(current, listened) { + return + } + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + glog.Warningf("remove mount socket %s: %v", socketPath, err) + } + }) + } +} + // serveMountGrpc exposes the local control socket used by "weed mount.stats". func serveMountGrpc(wfs *mount.WFS, listener net.Listener) { grpcS := pb.NewGrpcServer() diff --git a/weed/command/mount_common_test.go b/weed/command/mount_common_test.go index 48878cca53..a472f59178 100644 --- a/weed/command/mount_common_test.go +++ b/weed/command/mount_common_test.go @@ -5,6 +5,9 @@ package command import ( "bytes" "context" + "net" + "os" + "path/filepath" "testing" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" @@ -213,3 +216,42 @@ func Test_ensureBucketAllowEmptyFolders(t *testing.T) { }) } } + +func Test_mountSocketCleanup(t *testing.T) { + dir, err := os.MkdirTemp("", "swmnt") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + socketPath := filepath.Join(dir, "mount.sock") + listen := func() { + l, err := net.Listen("unix", socketPath) + if err != nil { + t.Skipf("unix sockets unavailable: %v", err) + } + t.Cleanup(func() { l.Close() }) + } + exists := func() bool { + _, err := os.Lstat(socketPath) + return err == nil + } + + listen() + removeMountSocket := mountSocketCleanup(socketPath) + removeMountSocket() + removeMountSocket() + if exists() { + t.Fatalf("%s is still there after the mount exited", socketPath) + } + + listen() + removeMountSocket = mountSocketCleanup(socketPath) + if err := os.Remove(socketPath); err != nil { + t.Fatal(err) + } + listen() + removeMountSocket() + if !exists() { + t.Fatalf("exiting mount removed the socket of a later mount of the same directory") + } +} diff --git a/weed/command/mount_std.go b/weed/command/mount_std.go index b047443c13..1988adcad3 100644 --- a/weed/command/mount_std.go +++ b/weed/command/mount_std.go @@ -69,10 +69,14 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { if err != nil { glog.Fatalf("Failed to listen on %s: %v", *option.localSocket, err) } + removeMountSocket := mountSocketCleanup(*option.localSocket) + defer removeMountSocket() + grace.OnInterrupt(removeMountSocket) // detect mount folder mode if *option.dirAutoCreate { if err := os.MkdirAll(dir, os.FileMode(0777)&^umask); err != nil { + removeMountSocket() glog.Fatalf("failed to create directory %s:%v", dir, err) } } @@ -113,6 +117,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { // Ensure target mount point availability skipAutofs := option.hasAutofs != nil && *option.hasAutofs if isValid := checkMountPointAvailable(dir, skipAutofs); !isValid { + removeMountSocket() glog.Fatalf("Target mount point is not available: %s, please check!", dir) return true } @@ -231,6 +236,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { if err != nil { // A failed mount is an environment problem (no /dev/fuse, fusermount not // setuid, stale mount point); the goroutine dump Fatalf adds buries it. + removeMountSocket() glog.Exitf("Mount fail: %v", err) } grace.OnInterrupt(func() { diff --git a/weed/command/mount_windows.go b/weed/command/mount_windows.go index 71cd6a3189..872278c3db 100644 --- a/weed/command/mount_windows.go +++ b/weed/command/mount_windows.go @@ -73,6 +73,9 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { if err != nil { glog.Fatalf("Failed to listen on %s: %v", *option.localSocket, err) } + removeMountSocket := mountSocketCleanup(*option.localSocket) + defer removeMountSocket() + grace.OnInterrupt(removeMountSocket) uidGidMapper, err := meta_cache.NewUidGidMapper(*option.uidMap, *option.gidMap) if err != nil {