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