diff options
Diffstat (limited to 'src/buildstream/_fuse/mount.py')
-rw-r--r-- | src/buildstream/_fuse/mount.py | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/src/buildstream/_fuse/mount.py b/src/buildstream/_fuse/mount.py new file mode 100644 index 000000000..e31684100 --- /dev/null +++ b/src/buildstream/_fuse/mount.py @@ -0,0 +1,196 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +import os +import signal +import time +import sys + +from contextlib import contextmanager +from multiprocessing import Process +from .fuse import FUSE + +from .._exceptions import ImplError +from .. import _signals + + +# Just a custom exception to raise here, for identifying possible +# bugs with a fuse layer implementation +# +class FuseMountError(Exception): + pass + + +# This is a convenience class which takes care of synchronizing the +# startup of FUSE and shutting it down. +# +# The implementations / subclasses should: +# +# - Overload the instance initializer to add any parameters +# needed for their fuse Operations implementation +# +# - Implement create_operations() to create the Operations +# instance on behalf of the superclass, using any additional +# parameters collected in the initializer. +# +# Mount objects can be treated as contextmanagers, the volume +# will be mounted during the context. +# +# UGLY CODE NOTE: +# +# This is a horrible little piece of code. The problem we face +# here is that the highlevel libfuse API has fuse_main(), which +# will either block in the foreground, or become a full daemon. +# +# With the daemon approach, we know that the fuse is mounted right +# away when fuse_main() returns, then the daemon will go and handle +# requests on its own, but then we have no way to shut down the +# daemon. +# +# With the blocking approach, we still have it as a child process +# so we can tell it to gracefully terminate; but it's impossible +# to know when the mount is done, there is no callback for that +# +# The solution we use here without digging too deep into the +# low level fuse API, is to fork a child process which will +# fun the fuse loop in foreground, and we block the parent +# process until the volume is mounted with a busy loop with timeouts. +# +class Mount(): + + # These are not really class data, they are + # just here for the sake of having None setup instead + # of missing attributes, since we do not provide any + # initializer and leave the initializer to the subclass. + # + __mountpoint = None + __operations = None + __process = None + + ################################################ + # User Facing API # + ################################################ + + def __init__(self, fuse_mount_options=None): + self._fuse_mount_options = {} if fuse_mount_options is None else fuse_mount_options + + # mount(): + # + # User facing API for mounting a fuse subclass implementation + # + # Args: + # (str): Location to mount this fuse fs + # + def mount(self, mountpoint): + + assert self.__process is None + + self.__mountpoint = mountpoint + self.__process = Process(target=self.__run_fuse) + + # Ensure the child fork() does not inherit our signal handlers, if the + # child wants to handle a signal then it will first set its own + # handler, and then unblock it. + with _signals.blocked([signal.SIGTERM, signal.SIGTSTP, signal.SIGINT], ignore=False): + self.__process.start() + + # This is horrible, we're going to wait until mountpoint is mounted and that's it. + while not os.path.ismount(mountpoint): + time.sleep(1 / 100) + + # unmount(): + # + # User facing API for unmounting a fuse subclass implementation + # + def unmount(self): + + # Terminate child process and join + if self.__process is not None: + self.__process.terminate() + self.__process.join() + + # Report an error if ever the underlying operations crashed for some reason. + if self.__process.exitcode != 0: + raise FuseMountError("{} reported exit code {} when unmounting" + .format(type(self).__name__, self.__process.exitcode)) + + self.__mountpoint = None + self.__process = None + + # mounted(): + # + # A context manager to run a code block with this fuse Mount + # mounted, this will take care of automatically unmounting + # in the case that the calling process is terminated. + # + # Args: + # (str): Location to mount this fuse fs + # + @contextmanager + def mounted(self, mountpoint): + + self.mount(mountpoint) + try: + with _signals.terminator(self.unmount): + yield + finally: + self.unmount() + + ################################################ + # Abstract Methods # + ################################################ + + # create_operations(): + # + # Create an Operations class (from fusepy) and return it + # + # Returns: + # (Operations): A FUSE Operations implementation + def create_operations(self): + raise ImplError("Mount subclass '{}' did not implement create_operations()" + .format(type(self).__name__)) + + ################################################ + # Child Process # + ################################################ + def __run_fuse(self): + + # First become session leader while signals are still blocked + # + # Then reset the SIGTERM handler to the default and finally + # unblock SIGTERM. + # + os.setsid() + signal.signal(signal.SIGTERM, signal.SIG_DFL) + signal.pthread_sigmask(signal.SIG_UNBLOCK, [signal.SIGTERM]) + + # Ask the subclass to give us an Operations object + # + self.__operations = self.create_operations() # pylint: disable=assignment-from-no-return + + # Run fuse in foreground in this child process, internally libfuse + # will handle SIGTERM and gracefully exit its own little main loop. + # + FUSE(self.__operations, self.__mountpoint, nothreads=True, foreground=True, nonempty=True, + **self._fuse_mount_options) + + # Explicit 0 exit code, if the operations crashed for some reason, the exit + # code will not be 0, and we want to know about it. + # + sys.exit(0) |