# Copyright Mercurial Contributors # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import annotations import functools import os import stat import time from typing import Optional, Tuple from .. import error rangemask = 0x7FFFFFFF @functools.total_ordering class timestamp(tuple): """ A Unix timestamp with optional nanoseconds precision, modulo 2**31 seconds. A 3-tuple containing: `truncated_seconds`: seconds since the Unix epoch, truncated to its lower 31 bits `subsecond_nanoseconds`: number of nanoseconds since `truncated_seconds`. When this is zero, the sub-second precision is considered unknown. `second_ambiguous`: whether this timestamp is still "reliable" (see `reliable_mtime_of`) if we drop its sub-second component. """ def __new__(cls, value): truncated_seconds, subsec_nanos, second_ambiguous = value value = (truncated_seconds & rangemask, subsec_nanos, second_ambiguous) return super(timestamp, cls).__new__(cls, value) def __eq__(self, other): raise error.ProgrammingError( 'timestamp should never be compared directly' ) def __gt__(self, other): raise error.ProgrammingError( 'timestamp should never be compared directly' ) def get_fs_now(vfs) -> Optional[timestamp]: """return a timestamp for "now" in the current vfs This will raise an exception if no temporary files could be created. """ tmpfd, tmpname = vfs.mkstemp() try: return mtime_of(os.fstat(tmpfd)) finally: os.close(tmpfd) vfs.unlink(tmpname) def zero() -> timestamp: """ Returns the `timestamp` at the Unix epoch. """ return tuple.__new__(timestamp, (0, 0)) def mtime_of(stat_result: os.stat_result) -> timestamp: """ Takes an `os.stat_result`-like object and returns a `timestamp` object for its modification time. """ try: # TODO: add this attribute to `osutil.stat` objects, # see `mercurial/cext/osutil.c`. # # This attribute is also not available on Python 2. nanos = stat_result.st_mtime_ns except AttributeError: # https://docs.python.org/2/library/os.html#os.stat_float_times # "For compatibility with older Python versions, # accessing stat_result as a tuple always returns integers." secs = stat_result[stat.ST_MTIME] subsec_nanos = 0 else: billion = int(1e9) secs = nanos // billion subsec_nanos = nanos % billion return timestamp((secs, subsec_nanos, False)) def reliable_mtime_of( stat_result: os.stat_result, present_mtime: timestamp ) -> Optional[timestamp]: """Wrapper for `make_mtime_reliable` for stat objects""" file_mtime = mtime_of(stat_result) return make_mtime_reliable(file_mtime, present_mtime) def make_mtime_reliable( file_timestamp: timestamp, present_mtime: timestamp ) -> Optional[timestamp]: """Same as `mtime_of`, but return `None` or a `Timestamp` with `second_ambiguous` set if the date might be ambiguous. A modification time is reliable if it is older than "present_time" (or sufficiently in the future). Otherwise a concurrent modification might happens with the same mtime. """ file_second = file_timestamp[0] file_ns = file_timestamp[1] boundary_second = present_mtime[0] boundary_ns = present_mtime[1] # If the mtime of the ambiguous file is younger (or equal) to the starting # point of the `status` walk, we cannot garantee that another, racy, write # will not happen right after with the same mtime and we cannot cache the # information. # # However if the mtime is far away in the future, this is likely some # mismatch between the current clock and previous file system operation. So # mtime more than one days in the future are considered fine. if boundary_second == file_second: if file_ns and boundary_ns: if file_ns < boundary_ns: return timestamp((file_second, file_ns, True)) return None elif boundary_second < file_second < (3600 * 24 + boundary_second): return None else: return file_timestamp FS_TICK_WAIT_TIMEOUT = 0.1 # 100 milliseconds def wait_until_fs_tick(vfs) -> Optional[Tuple[timestamp, bool]]: """Wait until the next update from the filesystem time by writing in a loop a new temporary file inside the working directory and checking if its time differs from the first one observed. Returns `None` if we are unable to get the filesystem time, `(timestamp, True)` if we've timed out waiting for the filesystem clock to tick, and `(timestamp, False)` if we've waited successfully. On Linux, your average tick is going to be a "jiffy", or 1/HZ. HZ is your kernel's tick rate (if it has one configured) and the value is the one returned by `grep 'CONFIG_HZ=' /boot/config-$(uname -r)`, again assuming a normal setup. In my case (Alphare) at the time of writing, I get `CONFIG_HZ=250`, which equates to 4ms. This might change with a series that could make it to Linux 6.12: https://lore.kernel.org/all/20241002-mgtime-v10-8-d1c4717f5284@kernel.org """ start = time.monotonic() try: old_fs_time = get_fs_now(vfs) new_fs_time = get_fs_now(vfs) while ( new_fs_time[0] == old_fs_time[0] and new_fs_time[1] == old_fs_time[1] ): if time.monotonic() - start > FS_TICK_WAIT_TIMEOUT: return (old_fs_time, True) new_fs_time = get_fs_now(vfs) except OSError: return None else: return (new_fs_time, False)