# # 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 . # # Authors: # Tristan Van Berkom 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)