1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
13 MountInfo = collections.namedtuple(
14 'MountInfo', ['is_fuse', 'major', 'minor', 'mnttype', 'path'])
19 with open('/proc/self/mountinfo') as f:
20 for m in f.readlines():
21 mntid, pmntid, dev, root, path, extra = m.split(" ", 5)
22 mnttype = extra.split(" - ")[1].split(" ", 1)[0]
23 major, minor = dev.split(":")
25 is_fuse=(mnttype == "fuse" or mnttype.startswith("fuse.")),
34 def paths_to_unmount(path, mnttype):
37 if m.path == path or m.path.startswith(path+"/"):
39 if not (m.is_fuse and (mnttype is None or
40 mnttype == m.mnttype)):
42 "cannot unmount {}: mount type is {}".format(
47 def safer_realpath(path, loop=True):
48 """Similar to os.path.realpath(), but avoids calling lstat().
50 Leaves some symlinks unresolved."""
53 elif not path.startswith('/'):
54 path = os.path.abspath(path)
56 path = path.rstrip('/')
57 dirname, basename = os.path.split(path)
59 path, resolved = safer_realpath(os.path.join(dirname, os.readlink(path)), loop=False)
61 # Path is not a symlink (EINVAL), or is unreadable, or
62 # doesn't exist. If the error was EINVAL and dirname can
63 # be resolved, we will have eliminated all symlinks and it
64 # will be safe to call normpath().
65 dirname, resolved = safer_realpath(dirname, loop=loop)
66 path = os.path.join(dirname, basename)
67 if resolved and e.errno == errno.EINVAL:
68 return os.path.normpath(path), True
73 # Unwind to the point where we first started following
76 # Resolving the whole path landed in a symlink cycle, but
77 # we might still be able to resolve dirname.
78 dirname, _ = safer_realpath(dirname, loop=loop)
79 return os.path.join(dirname, basename), False
82 def unmount(path, subtype=None, timeout=10, recursive=False):
83 """Unmount the fuse mount at path.
85 Unmounting is done by writing 1 to the "abort" control file in
86 sysfs to kill the fuse driver process, then executing "fusermount
87 -u -z" to detach the mount point, and repeating these steps until
88 the mount is no longer listed in /proc/self/mountinfo.
90 This procedure should enable a non-root user to reliably unmount
91 their own fuse filesystem without risk of deadlock.
93 Returns True if unmounting was successful, False if it wasn't a
94 fuse mount at all. Raises an exception if it cannot be unmounted.
97 path, _ = safer_realpath(path)
104 mnttype = 'fuse.' + subtype
107 paths = paths_to_unmount(path, mnttype)
109 # We might not have found any mounts merely because path
110 # contains symlinks, so we should resolve them and try
111 # again. We didn't do this from the outset because
112 # realpath() can hang (see explanation below).
113 paths = paths_to_unmount(os.path.realpath(path), mnttype)
114 for path in sorted(paths, key=len, reverse=True):
115 unmount(path, timeout=timeout, recursive=False)
116 return len(paths) > 0
120 fusermount_output = b''
124 deadline = time.time() + timeout
128 for m in mountinfo():
129 if m.is_fuse and (mnttype is None or mnttype == m.mnttype):
137 if not was_mounted and path != os.path.realpath(path):
138 # If the specified path contains symlinks, it won't appear
139 # verbatim in mountinfo.
141 # It might seem like we should have called realpath() from
142 # the outset. But we can't: realpath() hangs (in lstat())
143 # if we call it on an unresponsive mount point, and this
144 # is an important and common scenario.
146 # By waiting until now to try realpath(), we avoid this
147 # problem in the most common cases, which are: (1) the
148 # specified path has no symlinks and is a mount point, in
149 # which case was_mounted==True and we can proceed without
150 # calling realpath(); and (2) the specified path is not a
151 # mount point (e.g., it was already unmounted by someone
152 # else, or it's a typo), and realpath() can determine that
153 # without hitting any other unresponsive mounts.
154 path = os.path.realpath(path)
158 # This appears to avoid a race condition where we
159 # return control to the caller after running
160 # "fusermount -u -z" (see below), the caller (e.g.,
161 # arv-mount --replace) immediately tries to attach a
162 # new fuse mount at the same mount point, the
163 # lazy-unmount process unmounts that _new_ mount while
164 # it is being initialized, and the setup code waits
165 # forever for the new mount to be initialized.
170 # Report buffered stderr from previous call to fusermount,
171 # now that we know it didn't succeed.
172 sys.stderr.buffer.write(fusermount_output)
176 delay = min(delay, deadline - time.time())
178 raise Exception("timed out")
182 with open('/sys/fs/fuse/connections/{}/abort'.format(m.minor),
186 if e.errno != errno.ENOENT:
191 subprocess.check_output(
192 ["fusermount", "-u", "-z", path],
193 stderr=subprocess.STDOUT)
194 except subprocess.CalledProcessError as e:
195 fusermount_output = e.output
197 fusermount_output = b''