1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
// +build linux
// Forcibly abort a FUSE filesystem mounted at the given path.
//
// This is only supported on Linux.
package main
import (
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/anacrolix/fuse"
"github.com/anacrolix/fuse/cmd/fuse-abort/internal/mountinfo"
)
// When developing a FUSE filesystem, it's pretty common to end up
// with broken mount points, where the FUSE server process is either
// no longer running, or is not responsive.
//
// The usual `fusermount -u` / `umount` commands do things like stat
// the mountpoint, causing filesystem requests. A hung filesystem
// won't answer them.
//
// The way out of this conundrum is to sever the kernel FUSE
// connection. This process is woefully underdocumented, but basically
// we need to find a "connection identifier" and then use `sysfs` to
// tell the FUSE kernelspace to abort the connection.
//
// The special sauce is knowing that the minor number of a device node
// for the mountpoint is this identifier. That and some careful
// parsing of a file listing all the mounts.
//
// https://www.kernel.org/doc/Documentation/filesystems/fuse.txt
// https://sourceforge.net/p/fuse/mailman/message/31426925/
// findFUSEMounts returns a mapping of all the known mounts in the
// current namespace. For FUSE mounts, the value will be the
// connection ID. Non-FUSE mounts store an empty string, to
// differentiate error messages.
func findFUSEMounts() (map[string]string, error) {
r := map[string]string{}
mounts, err := mountinfo.Open(mountinfo.DefaultPath)
if err != nil {
return nil, fmt.Errorf("cannot open mountinfo: %v", err)
}
defer mounts.Close()
for {
info, err := mounts.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("parsing mountinfo: %v", err)
}
if info.FSType != "fuse" && !strings.HasPrefix(info.FSType, "fuse.") {
r[info.Mountpoint] = ""
continue
}
if info.Major != "0" {
return nil, fmt.Errorf("FUSE mount has weird device major number: %v:%v: %v", info.Major, info.Minor, info.Mountpoint)
}
if _, ok := r[info.Mountpoint]; ok {
return nil, fmt.Errorf("mountpoint seen seen twice in mountinfo: %v", info.Mountpoint)
}
r[info.Mountpoint] = info.Minor
}
return r, nil
}
func abort(id string) error {
p := filepath.Join("/sys/fs/fuse/connections", id, "abort")
f, err := os.OpenFile(p, os.O_WRONLY, 0600)
if errors.Is(err, os.ErrNotExist) {
// nothing to abort, consider that a success because we might
// have just raced against an unmount
return nil
}
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("1\n"); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
f = nil
return nil
}
func pruneEmptyDir(p string) error {
// we want an rmdir and not a generic delete like
// os.Remove; the node underlying the mountpoint might not
// be a directory, and we really want to only prune
// directories
if err := syscall.Rmdir(p); err != nil {
switch err {
case syscall.ENOTEMPTY, syscall.ENOTDIR:
// underlying node wasn't an empty dir; ignore
case syscall.ENOENT:
// someone else removed it for us; ignore
default:
err = &os.PathError{
Op: "rmdir",
Path: p,
Err: err,
}
return err
}
}
return nil
}
var errWarnings = errors.New("encountered warnings")
func run(prune bool, mountpoints []string) error {
success := true
// make an explicit effort to process mountpoints in command line
// order, even if mountinfo is not in that order
mounts, err := findFUSEMounts()
if err != nil {
return err
}
for _, mountpoint := range mountpoints {
p, err := filepath.Abs(mountpoint)
if err != nil {
log.Printf("cannot make path absolute: %s: %v", mountpoint, err)
success = false
continue
}
id, ok := mounts[p]
if !ok {
log.Printf("mountpoint not found: %v", p)
success = false
continue
}
if id == "" {
log.Printf("not a FUSE mount: %v", p)
success = false
continue
}
if err := abort(id); err != nil {
return fmt.Errorf("cannot abort: %v is connection %v: %v", p, id, err)
}
if err := fuse.Unmount(p); err != nil {
log.Printf("cannot unmount: %v", err)
success = false
continue
}
if prune {
if err := pruneEmptyDir(p); err != nil {
log.Printf("cannot prune mountpoint: %v", err)
success = false
}
}
}
if !success {
return errWarnings
}
return nil
}
var prog = filepath.Base(os.Args[0])
func usage() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", prog)
fmt.Fprintf(flag.CommandLine.Output(), " %s MOUNTPOINT..\n", prog)
fmt.Fprintf(flag.CommandLine.Output(), "\n")
fmt.Fprintf(flag.CommandLine.Output(), "Forcibly aborts a FUSE filesystem mounted at the given path.\n")
fmt.Fprintf(flag.CommandLine.Output(), "\n")
flag.PrintDefaults()
}
func main() {
log.SetFlags(0)
log.SetPrefix(prog + ": ")
var prune bool
flag.BoolVar(&prune, "p", false, "prune empty mountpoints after unmounting")
flag.Usage = usage
flag.Parse()
if flag.NArg() == 0 {
flag.Usage()
os.Exit(2)
}
if err := run(prune, flag.Args()); err != nil {
if err == errWarnings {
// they've already been logged
os.Exit(1)
}
log.Fatal(err)
}
}
|