mirror of
				https://github.com/ARM-software/workload-automation.git
				synced 2025-10-31 15:12:25 +00:00 
			
		
		
		
	utils/misc: Implement atomic writes
To simulate atomic writes, use a context manager to write to a temporary file location and then rename over the original file. This is performed using the `safe_move` method which performs this operation and handles cases where the source and destination are on separate file systems.
This commit is contained in:
		| @@ -19,6 +19,7 @@ Miscellaneous functions that don't fit anywhere else. | |||||||
|  |  | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  | import errno | ||||||
| import hashlib | import hashlib | ||||||
| import imp | import imp | ||||||
| import logging | import logging | ||||||
| @@ -26,15 +27,17 @@ import math | |||||||
| import os | import os | ||||||
| import random | import random | ||||||
| import re | import re | ||||||
|  | import shutil | ||||||
| import string | import string | ||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
| import traceback | import traceback | ||||||
|  | import uuid | ||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from functools import reduce  # pylint: disable=redefined-builtin | from functools import reduce  # pylint: disable=redefined-builtin | ||||||
| from operator import mul | from operator import mul | ||||||
| from tempfile import gettempdir | from tempfile import gettempdir, NamedTemporaryFile | ||||||
| from time import sleep | from time import sleep | ||||||
| if sys.version_info[0] == 3: | if sys.version_info[0] == 3: | ||||||
|     from io import StringIO |     from io import StringIO | ||||||
| @@ -58,6 +61,7 @@ from devlib.utils.misc import (ABI_MAP, check_output, walk_modules, | |||||||
| check_output_logger = logging.getLogger('check_output') | check_output_logger = logging.getLogger('check_output') | ||||||
|  |  | ||||||
| file_lock_logger = logging.getLogger('file_lock') | file_lock_logger = logging.getLogger('file_lock') | ||||||
|  | at_write_logger = logging.getLogger('at_write') | ||||||
|  |  | ||||||
|  |  | ||||||
| # Defined here rather than in wa.exceptions due to module load dependencies | # Defined here rather than in wa.exceptions due to module load dependencies | ||||||
| @@ -647,6 +651,63 @@ def format_ordered_dict(od): | |||||||
|                                      for k, v in od.items())) |                                      for k, v in od.items())) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @contextmanager | ||||||
|  | def atomic_write_path(path, mode='w'): | ||||||
|  |     """ | ||||||
|  |     Gets a file path to write to which will be replaced with the original | ||||||
|  |      file path to simulate an atomic write from the point of view | ||||||
|  |     of other processes. This is achieved by writing to a tmp file and | ||||||
|  |     replacing the exiting file to prevent inconsistencies. | ||||||
|  |     """ | ||||||
|  |     tmp_file = None | ||||||
|  |     try: | ||||||
|  |         tmp_file = NamedTemporaryFile(mode=mode, delete=False, | ||||||
|  |                                       suffix=os.path.basename(path)) | ||||||
|  |         at_write_logger.debug('') | ||||||
|  |         yield tmp_file.name | ||||||
|  |         os.fsync(tmp_file.file.fileno()) | ||||||
|  |     finally: | ||||||
|  |         if tmp_file: | ||||||
|  |             tmp_file.close() | ||||||
|  |     at_write_logger.debug('Moving {} to {}'.format(tmp_file.name, path)) | ||||||
|  |     safe_move(tmp_file.name, path) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def safe_move(src, dst): | ||||||
|  |     """ | ||||||
|  |     Taken from: https://alexwlchan.net/2019/03/atomic-cross-filesystem-moves-in-python/ | ||||||
|  |  | ||||||
|  |     Rename a file from ``src`` to ``dst``. | ||||||
|  |  | ||||||
|  |     *   Moves must be atomic.  ``shutil.move()`` is not atomic. | ||||||
|  |     *   Moves must work across filesystems and ``os.rename()`` can | ||||||
|  |         throw errors if run across filesystems. | ||||||
|  |  | ||||||
|  |     So we try ``os.rename()``, but if we detect a cross-filesystem copy, we | ||||||
|  |     switch to ``shutil.move()`` with some wrappers to make it atomic. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         os.rename(src, dst) | ||||||
|  |     except OSError as err: | ||||||
|  |  | ||||||
|  |         if err.errno == errno.EXDEV: | ||||||
|  |             # Generate a unique ID, and copy `<src>` to the target directory | ||||||
|  |             # with a temporary name `<dst>.<ID>.tmp`.  Because we're copying | ||||||
|  |             # across a filesystem boundary, this initial copy may not be | ||||||
|  |             # atomic.  We intersperse a random UUID so if different processes | ||||||
|  |             # are copying into `<dst>`, they don't overlap in their tmp copies. | ||||||
|  |             copy_id = uuid.uuid4() | ||||||
|  |             tmp_dst = "%s.%s.tmp" % (dst, copy_id) | ||||||
|  |             shutil.copyfile(src, tmp_dst) | ||||||
|  |  | ||||||
|  |             # Then do an atomic rename onto the new name, and clean up the | ||||||
|  |             # source image. | ||||||
|  |             os.rename(tmp_dst, dst) | ||||||
|  |             os.unlink(src) | ||||||
|  |         else: | ||||||
|  |             raise | ||||||
|  |  | ||||||
|  |  | ||||||
| @contextmanager | @contextmanager | ||||||
| def lock_file(path, timeout=30): | def lock_file(path, timeout=30): | ||||||
|     """ |     """ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user