Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Global flock file accessible by every user / Introducing FlockContext manager #1697

Merged
merged 14 commits into from
May 18, 2024
2 changes: 1 addition & 1 deletion .codespellrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[codespell]
# Folders and files to skip
skip = .codespellrc,*.po,Makefile,*.desktop,.git,__pycache__,*.pyc,languages.py
skip = .codespellrc,*.po,Makefile,*.desktop,.git,__pycache__,*.pyc,languages.py,*.html,_build
# Print N lines of surrounding context
context = 1
# Check hidden files also (empty means True)
Expand Down
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Back In Time

Version 1.4.4-dev (development of upcoming release)
* Fix: Global flock for multiple users (#1122, #1676)
* Fix bug: "Backup folders" list does reflect the selected snapshot (#1585) (@rafaelhdr Rafael Hurpia da Rocha)
* Breaking Change: GUI started with --debug does no longer add --debug to the crontab for scheduled profiles.
Use the new "enable logging for debug messages" in the 'Schedule' section of the 'Manage profiles' GUI instead.
Expand Down
38 changes: 23 additions & 15 deletions common/applicationinstance.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
# Back In Time
# Copyright (C) 2008-2022 Oprea Dan, Bart de Koning, Richard Bailey, Germar Reitze
# Back In Time
# Copyright (C) 2008-2022 Oprea Dan, Bart de Koning, Richard Bailey,
# Germar Reitze
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""
This module holds the ApplicationInstance class, used to handle
the one application instance mechanism
the one application instance mechanism.
"""

import os
Expand All @@ -30,6 +31,8 @@
# one app instance eg. if a restore is running and another
# backup starts).
# Rename it to eg. LockFileManager
# TODO2 When refactoring have a look at "common/flock.py" still implementing
# a contxt manager for that problem.
class ApplicationInstance:
"""
Class used to handle one application instance mechanism.
Expand Down Expand Up @@ -154,15 +157,20 @@ def flockExclusiv(self):
(so that only the last creator wins).

Dev notes:
---------
----------
buhtz (2023-09):
Not sure but just log an ERROR without doing anything else is
IMHO not enough.

aryoda (2023-12):
It seems the purpose of this additional lock file using an exclusive lock
is to block the other process to continue until this exclusive lock
is released (= serialize execution).
Therefore advisory locks are used via fcntl.flock (see: man 2 fcntl)

buhtz (2024-05):
Have a look at the new :mod:`flock` module providing an flock context
manager.
"""

flock_file_URI = self.pidFile + '.flock'
Expand Down
7 changes: 7 additions & 0 deletions common/doc-dev/flock.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
flock module
============

.. automodule:: flock
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions common/doc-dev/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ common
diagnostics
encfstools
exceptions
flock
guiapplicationinstance
logger
mount
Expand Down
74 changes: 74 additions & 0 deletions common/flock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SPDX-FileCopyrightText: © 2024 Christian BUHTZ <c.buhtz@posteo.jp>
buhtz marked this conversation as resolved.
Show resolved Hide resolved
#
# SPDX-License-Identifier: GPL-2.0
#
# 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 <https://www.gnu.org/licenses/#GPL>.
import os
import fcntl
from pathlib import Path
import logger


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`.

Usage example ::

class MyFlock(_FlockContext):
def __init__(self):
super().__init__('my.lock')

with MyFlock():
do_fancy_things()

"""
def __init__(self, filename: str, folder: Path = None):
if folder is None:
folder = Path(Path.cwd().root) / 'run' / 'lock'

# out-dated default
if not folder.exists():
folder = Path(Path.cwd().root) / 'var' / 'lock'

self._file_path = folder / filename
"""Path to used for flock"""

def __enter__(self):
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)

# 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):
self._log('Release')
fcntl.fcntl(self._flock_handle, fcntl.LOCK_UN)
self._flock_handle.close()

def _log(self, prefix: str):
"""Generate a log message including the current lock files path and the
process ID.
"""
logger.debug(f'{prefix} flock {self._file_path} by PID {os.getpid()}',
self)


class GlobalFlock(_FlockContext):
"""Flock context manager used for global flock in Back In Time."""
def __init__(self):
super().__init__('backintime.lock')