From d56f0fbe204127232e5ce6d4aff347dd8a5c99c0 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Fri, 3 Jan 2020 11:15:21 +0000 Subject: [PATCH] utils/misc: Add file locking context manager Enable automation locking and unlocking of a file path provided. Used to prevent synchronisation issues between multiple wa processes. --- wa/utils/misc.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/wa/utils/misc.py b/wa/utils/misc.py index 9f858604..033d6216 100644 --- a/wa/utils/misc.py +++ b/wa/utils/misc.py @@ -30,9 +30,11 @@ import string import subprocess import sys import traceback +from contextlib import contextmanager from datetime import datetime, timedelta from functools import reduce # pylint: disable=redefined-builtin from operator import mul +from time import sleep if sys.version_info[0] == 3: from io import StringIO else: @@ -54,6 +56,8 @@ from devlib.utils.misc import (ABI_MAP, check_output, walk_modules, check_output_logger = logging.getLogger('check_output') +file_lock_logger = logging.getLogger('file_lock') + # Defined here rather than in wa.exceptions due to module load dependencies def diff_tokens(before_token, after_token): @@ -640,3 +644,43 @@ def format_ordered_dict(od): """ return '{{{}}}'.format(', '.join('{}={}'.format(k, v) for k, v in od.items())) + + +@contextmanager +def lock_file(path, timeout=30): + """ + Enable automatic locking and unlocking of a file path given. Used to + prevent synchronisation issues between multiple wa processes. + Uses a default timeout of 30 seconds which should be overridden for files + that are expect to be unavailable for longer periods of time. + """ + + # Import here to avoid circular imports + # pylint: disable=wrong-import-position,cyclic-import + from wa.framework.exception import ResourceError + + locked = False + l_file = '{}.lock'.format(path) if not path.endswith('.lock') else path + file_lock_logger.debug('Acquiring lock on "{}"'.format(l_file)) + try: + while timeout: + try: + open(l_file, 'x').close() + locked = True + file_lock_logger.debug('Lock acquired on "{}"'.format(l_file)) + break + except FileExistsError: + msg = 'Failed to acquire lock on "{}" Retrying...' + file_lock_logger.debug(msg.format(l_file)) + sleep(1) + timeout -= 1 + else: + msg = 'Failed to acquire lock file "{}" within the timeout. \n' \ + 'If there are no other running WA processes please delete ' \ + 'this file and retry.' + raise ResourceError(msg.format(os.path.abspath(l_file))) + yield + finally: + if locked and os.path.exists(l_file): + os.remove(l_file) + file_lock_logger.debug('Lock released "{}"'.format(l_file))