blob: ce085123fbc6867ac6d99535a303097e448ef1c1 [file] [log] [blame]
#
# 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, utils
# 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 start a child process which will
# run 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
__logfile = 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, args=(self.__logfile.name,))
# Ensure the child process 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):
# Note that the temporary __logfile isn't picklable, so we must make
# sure it is not part of `self` when spawning a process.
logfile = self.__logfile
self.__logfile = None
self.__process.start()
self.__logfile = logfile
while not os.path.ismount(mountpoint):
if not self.__process.is_alive():
self.__logfile.seek(0)
stderr = self.__logfile.read()
raise FuseMountError("Unable to mount {}: {}".format(mountpoint, stderr.decode().strip()))
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:
self.__logfile.seek(0)
stderr = self.__logfile.read()
raise FuseMountError("{} reported exit code {} when unmounting: {}"
.format(type(self).__name__, self.__process.exitcode, stderr))
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):
with utils._tempnamedfile() as logfile:
self.__logfile = logfile
self.__mount(mountpoint)
try:
with _signals.terminator(self.__unmount):
yield
finally:
self.__unmount()
self.__logfile = None
################################################
# 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, filename):
# Override stdout/stderr to the file given as a pointer, that way our parent process can get our output
out = open(filename, "w")
os.dup2(out.fileno(), sys.stdout.fileno())
os.dup2(out.fileno(), sys.stderr.fileno())
# 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.
#
try:
FUSE(self.__operations, self.__mountpoint, nothreads=True, foreground=True, nonempty=True,
**self._fuse_mount_options)
except RuntimeError as exc:
# FUSE will throw a RuntimeError with the exit code of libfuse as args[0]
sys.exit(exc.args[0])
# 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)