Merge branch '8784-dir-listings'
[arvados.git] / services / fuse / arvados_fuse / unmount.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 import collections
6 import errno
7 import os
8 import subprocess
9 import time
10
11
12 MountInfo = collections.namedtuple(
13     'MountInfo', ['is_fuse', 'major', 'minor', 'mnttype', 'path'])
14
15
16 def mountinfo():
17     mi = []
18     with open('/proc/self/mountinfo') as f:
19         for m in f.readlines():
20             mntid, pmntid, dev, root, path, extra = m.split(" ", 5)
21             mnttype = extra.split(" - ")[1].split(" ", 1)[0]
22             major, minor = dev.split(":")
23             mi.append(MountInfo(
24                 is_fuse=(mnttype == "fuse" or mnttype.startswith("fuse.")),
25                 major=major,
26                 minor=minor,
27                 mnttype=mnttype,
28                 path=path,
29             ))
30     return mi
31
32
33 def unmount(path, subtype=None, timeout=10, recursive=False):
34     """Unmount the fuse mount at path.
35
36     Unmounting is done by writing 1 to the "abort" control file in
37     sysfs to kill the fuse driver process, then executing "fusermount
38     -u -z" to detach the mount point, and repeating these steps until
39     the mount is no longer listed in /proc/self/mountinfo.
40
41     This procedure should enable a non-root user to reliably unmount
42     their own fuse filesystem without risk of deadlock.
43
44     Returns True if unmounting was successful, False if it wasn't a
45     fuse mount at all. Raises an exception if it cannot be unmounted.
46     """
47
48     path = os.path.realpath(path)
49
50     if subtype is None:
51         mnttype = None
52     elif subtype == '':
53         mnttype = 'fuse'
54     else:
55         mnttype = 'fuse.' + subtype
56
57     if recursive:
58         paths = []
59         for m in mountinfo():
60             if m.path == path or m.path.startswith(path+"/"):
61                 paths.append(m.path)
62                 if not (m.is_fuse and (mnttype is None or
63                                        mnttype == m.mnttype)):
64                     raise Exception(
65                         "cannot unmount {}: mount type is {}".format(
66                             path, m.mnttype))
67         for path in sorted(paths, key=len, reverse=True):
68             unmount(path, timeout=timeout, recursive=False)
69         return len(paths) > 0
70
71     was_mounted = False
72     attempted = False
73     if timeout is None:
74         deadline = None
75     else:
76         deadline = time.time() + timeout
77
78     while True:
79         mounted = False
80         for m in mountinfo():
81             if m.is_fuse and (mnttype is None or mnttype == m.mnttype):
82                 try:
83                     if os.path.realpath(m.path) == path:
84                         was_mounted = True
85                         mounted = True
86                         break
87                 except OSError:
88                     continue
89         if not mounted:
90             return was_mounted
91
92         if attempted:
93             delay = 1
94             if deadline:
95                 delay = min(delay, deadline - time.time())
96                 if delay <= 0:
97                     raise Exception("timed out")
98             time.sleep(delay)
99
100         try:
101             with open('/sys/fs/fuse/connections/{}/abort'.format(m.minor),
102                       'w') as f:
103                 f.write("1")
104         except OSError as e:
105             if e.errno != errno.ENOENT:
106                 raise
107
108         attempted = True
109         try:
110             subprocess.check_call(["fusermount", "-u", "-z", path])
111         except subprocess.CalledProcessError:
112             pass