diff --git a/CHANGES b/CHANGES index 58fcdf791..972d016f4 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,7 @@ Back In Time Version 1.4.4-dev (development of upcoming release) +* Fix: Global flock fallback to single-user mode if insufficient permissions (#1743, #1751) * Chore!: Remove "debian" folder (#1548) * Breaking Change: EncFS deprecation warning (#1735, #1734) * Fix: Fix Qt segmentation fault with uninstall ExtraMouseButtonEventFilter when closing main window (#1095) diff --git a/TRANSLATIONS b/TRANSLATIONS index 791ecafa2..add839ca1 100644 --- a/TRANSLATIONS +++ b/TRANSLATIONS @@ -4,7 +4,9 @@ Bokmål (Norwegian) [nb_NO]: Hans Fredrik Nordhaug Catalan [ca]: Josep Sanchez Chinese (Simplified) [zh_CN,zh_Hans]: Kuntao Zhao Chinese (Traditional) [zh_TW,zh_Hant]: Kuntao Zhao +Danish [da]: Adam Sjøgren German [de]: Michael Wiedmann +Greek [el]: Iliana Panagopoulou (hpanago) French [fr]: Michel Corps , jej@github Indonesian [id]: Andika Triwidada Japanese [ja]: diff --git a/common/doc-dev/conf.py b/common/doc-dev/conf.py index 74c8cd5e9..65a156daa 100644 --- a/common/doc-dev/conf.py +++ b/common/doc-dev/conf.py @@ -1,18 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# BackInTime documentation build configuration file, created by -# sphinx-quickstart on Sat Jan 9 00:04:35 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - import sys import os @@ -24,17 +10,11 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.pardir))) sys.path.insert(0, os.path.abspath(os.path.join(os.pardir, "plugins"))) -#import config to solve race conditions between config an mount +# Import to solve race conditions between config an mount. import config # -- General configuration ------------------------------------------------ -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', @@ -49,9 +29,6 @@ # The suffix of source filenames. source_suffix = '.rst' -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' @@ -66,43 +43,21 @@ # The full version, including alpha/beta/rc tags. release = version # '1.3.3-dev' -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +autodoc_default_options = { + 'members': True, + 'member-order': 'bysource', + 'private-members': True, + 'undoc-members': True, + 'special-members': True, + 'exclude-members': '__weakref__,__dict__,__module__,__annotations__', +} # -- Intersphinx options -------------------------------------------------- @@ -113,95 +68,26 @@ } # -- Napoleon include private members which have docstrings --------------- - napoleon_include_private_with_doc = True # -- Options for HTML output ---------------------------------------------- - # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'classic' html_theme = 'sphinx_rtd_theme' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y, %H:%M (%Z)' -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - # Output file base name for HTML help builder. htmlhelp_basename = 'BackInTimeDevDoc' - # -- Options for LaTeX output --------------------------------------------- latex_elements = { @@ -223,27 +109,6 @@ 'Germar Reitze', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples @@ -253,10 +118,6 @@ ['Germar Reitze'], 1) ] -# If true, show URL addresses after external links. -#man_show_urls = False - - # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples @@ -267,17 +128,3 @@ 'Germar Reitze', 'BackInTime', 'One line description of project.', 'Miscellaneous'), ] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. diff --git a/common/flock.py b/common/flock.py index baa8f6de3..ec794d1f7 100644 --- a/common/flock.py +++ b/common/flock.py @@ -5,6 +5,10 @@ # This file is part of the program "Back In time" which is released under GNU # General Public License v2 (GPLv2). # See file LICENSE or go to . +"""Manage file lock. + +Offer context managers to manage file lock (flock) files. +""" import os import fcntl from pathlib import Path @@ -14,8 +18,10 @@ class _FlockContext: """Context manager to manage file locks (flock). - The flock file is stored in the folder `/run/lock` or if not present in - `/var/lock`. + It will be tried to establish a multi-user file lock; if not feasible a + single-user file lock will be used. It depends on the GNU Linux + distribution used and the write permissions to the file lock locations in + the file system. Usage example :: @@ -26,36 +32,138 @@ def __init__(self): with MyFlock(): do_fancy_things() + The following directories will be checked in sequence to determine if they + exist, if a file lock file exists within them, or if there are sufficient + permissions to create such a file within them. :: + + /run/lock + /var/lock + /run/user// + ~/.cache + + The first and second directory in that list is for multi-user file lock. + + To the experience of the developers on Debian-based distributions there is + no problem having a multi-user file lock. But on Arch-based distributions + only a user with root privileges is able to do it. Because of that on Arch + a single-user file lock is used by default until Back In Time is started + once as root. """ - def __init__(self, filename: str, folder: Path = None): - if folder is None: - folder = Path(Path.cwd().root) / 'run' / 'lock' + def __init__(self, + filename: str, + disable: bool = False): + """Check if an flock file can be used or created. + + See the classes documentation about details. + + Args: + filename: The filename (without path) used for the flock file. + disabled: Disable the whole context managers behavior. This is a + workaround. See #1751 and :func:``Snapshots.backup()`` for + details. + + Raises: + RuntimeError: If it wasn't possible to use + """ + self._file_path = None + """Full path used for the flock file""" + + self._flock_handle = None + """File handle (descriptor) to the flock file.""" - # out-dated default - if not folder.exists(): - folder = Path(Path.cwd().root) / 'var' / 'lock' + # Workaround for #1751. Remove after refactoring Snapshots.backup() + if disable: + return + + folder = Path(Path.cwd().root) / 'run' / 'lock' + + if not folder.exists(): + # On older systems + folder = Path(Path.cwd().root) / 'var' / 'lock' self._file_path = folder / filename - """Path to used for flock""" + + if self._can_use_file(self._file_path): + return + + # Try user specific file lock + # e.g. /run/user/ + self._file_path = Path(os.environ['XDG_RUNTIME_DIR']) / filename + + if self._can_use_file(self._file_path): + return + + # At last, try users cache dir. + self._file_path = Path( + os.environ.get('XDG_CACHE_HOME', + Path.home() / 'cache') + ) / filename + + if self._can_use_file(self._file_path): + return + + raise RuntimeError( + f'Can not establish global flock file {self._file_path}') + + def _can_use_file(self, file_path: Path) -> bool: + """Check if ``file_path`` is usable as an flock file. + + The answer is ``True`` if the file exists without checking its + permissions. If not the file will be created and if successful + ``True`` will be returned. + + Returns: + bool: The answer. + + Raises: + PermissionError: Not enough permissions to create the file. + Exception: Any other error. + """ + if file_path.exists(): + return True + + # Try to create it + try: + file_path.touch(mode=0o666) + + except PermissionError: + logger.debug(f'Cannot use file lock on {file_path}.') + + except Exception as err: + logger.error( + f'Unknown error while testing file lock on {file_path}. ' + f'Please open a bug report. Error was {err}.') + + else: + logger.debug(f'Use {file_path} for file lock.') + return True + + return False def __enter__(self): + """Request an exclucive file lock on :data:``self._file_path``. + """ + # Workaround for #1751. Remove after refactoring Snapshots.backup() + # See __init__() for details + if self._file_path is None: + return + self._log('Set') - # Open (and create if needed) the file - mode = 'r' if self._file_path.exists() else 'w' - self._flock_handle = self._file_path.open(mode) + # Open file for reading + self._flock_handle = self._file_path.open(mode='r') # blocks (waits) until an existing flock is released fcntl.flock(self._flock_handle, fcntl.LOCK_EX) - # If new created file set itspermissions to "rw-rw-rw". - # otherwise a foreign user is not able to use it. - if mode == 'w': - self._file_path.chmod(int('0o666', 8)) - return self def __exit__(self, exc_type, exc_value, exc_tb): + # Workaround for #1751. Remove after refactoring Snapshots.backup() + # See __init__() for details + if self._flock_handle is None: + return + self._log('Release') fcntl.fcntl(self._flock_handle, fcntl.LOCK_UN) self._flock_handle.close() @@ -63,12 +171,20 @@ def __exit__(self, exc_type, exc_value, exc_tb): def _log(self, prefix: str): """Generate a log message including the current lock files path and the process ID. + + Args: + prefix: Used in front of the log message. """ - logger.debug(f'{prefix} flock {self._file_path} by PID {os.getpid()}', - self) + logger.debug(f'{prefix} flock {self._file_path} by PID {os.getpid()}') class GlobalFlock(_FlockContext): - """Flock context manager used for global flock in Back In Time.""" - def __init__(self): - super().__init__('backintime.lock') + """Context manager used for global file lock in Back In Time. + + If it is a multi-user or single-user flock depends on the several + aspects. See :class:`_FlockContext` for details. + """ + def __init__(self, disable: bool = False): + """See :func:`_FlockContext.__init__()` for details. + """ + super().__init__('backintime.lock', disable=disable) diff --git a/common/snapshots.py b/common/snapshots.py index dec06357f..5ecddd6e1 100644 --- a/common/snapshots.py +++ b/common/snapshots.py @@ -779,10 +779,11 @@ def backup(self, force=False): instance.startApplication() - # global flock to block backups from other profiles or users - # (and run them serialized) - # self.flockExclusive() - with flock.GlobalFlock(): + # Global flock to block backups from other profiles or users + # (and run them serialized). The argument "disabled" is a + # workaround (#1751) that should be removed/refactored after + # this method ("backup()") is refactored. + with flock.GlobalFlock(disable=not self.config.globalFlock()): logger.info('Lock', self) now = datetime.datetime.today() diff --git a/common/tools.py b/common/tools.py index 4bd87790f..d9383869f 100644 --- a/common/tools.py +++ b/common/tools.py @@ -86,19 +86,20 @@ # | Handling paths | # |-----------------| - def sharePath(): """Get path where Back In Time is installed. - This is similar to $XDG_DATA_DIRS (XDG Base Directory Specification). - If running from source return default ``/usr/share``. + This is similar to ``XDG_DATA_DIRS``. If running from source return + default ``/usr/share``. - Returns: - str: share path like:: + Share path like: :: - /usr/share - /usr/local/share - /opt/usr/share + /usr/share + /usr/local/share + /opt/usr/share + + Returns: + str: Share path. """ share = os.path.abspath( os.path.join(__file__, os.pardir, os.pardir, os.pardir) @@ -112,16 +113,14 @@ def sharePath(): def backintimePath(*path): """ - Get path inside 'backintime' install folder. + Get path inside ``backintime`` install folder. Args: - *path (str): paths that should be joined to 'backintime' + *path (str): Paths that should be joined to ``backintime``. Returns: - str: 'backintime' child path like:: - - /usr/share/backintime/common - /usr/share/backintime/qt + str: Child path of ``backintime`` child path e.g. + ``/usr/share/backintime/common``or ``/usr/share/backintime/qt``. """ return os.path.abspath(os.path.join(__file__, os.pardir, os.pardir, *path)) @@ -2013,20 +2012,17 @@ class Alarm(object): (reentrance) or you may cause non-deterministic "random" RTEs. """ def __init__(self, callback = None, overwrite = True): - """ - Create a new alarm instance + """Create a new alarm instance Args: - callback: Function to call when the timer ran down - (ensure calling only reentrant code). - Use ``None`` to throw a ``Timeout`` exception instead. - overwrite: Is it allowed to (re)start the timer - even though the current timer is still running - ("ticking"): - ``True`` cancels the current timer (if active) - and restarts with the new timeout. - ``False` silently ignores the start request - if the current timer is still "ticking" + callback: Function to call when the timer ran down (ensure + calling only reentrant code). Use ``None`` to throw a + ``Timeout`` exception instead. + overwrite: Is it allowed to (re)start the timer even though the + current timer is still running ("ticking"). ``True`` cancels + the current timer (if active) and restarts with the new + timeout. ``False`` silently ignores the start request if the + current timer is still "ticking" """ self.callback = callback self.ticking = False