lock.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. # Copyright (C) 2012 Anaconda, Inc
  2. # SPDX-License-Identifier: BSD-3-Clause
  3. """
  4. Tools for working with locks
  5. A lock is just an empty directory. We use directories because this lets us use
  6. the race condition-proof os.makedirs.
  7. For now, there is one global lock for all of conda, because some things happen
  8. globally (such as downloading packages).
  9. We don't raise an error if the lock is named with the current PID
  10. """
  11. import logging
  12. import os
  13. import time
  14. from glob import glob
  15. from os.path import abspath, basename, dirname, isdir, join
  16. from .deprecations import deprecated
  17. from .exceptions import LockError
  18. deprecated.module("23.3", "23.9", addendum="Use `filelock` instead.")
  19. LOCK_EXTENSION = "conda_lock"
  20. # Keep the string "LOCKERROR" in this string so that external
  21. # programs can look for it.
  22. LOCKSTR = """
  23. LOCKERROR: It looks like conda is already doing something.
  24. The lock {0} was found. Wait for it to finish before continuing.
  25. If you are sure that conda is not running, remove it and try again.
  26. You can also use: $ conda clean --lock
  27. """
  28. log = logging.getLogger(__name__)
  29. stdoutlog = logging.getLogger("conda.stdoutlog")
  30. def touch(file_name, times=None):
  31. """Touch function like touch in Unix shell
  32. :param file_name: the name of file
  33. :param times: the access and modified time
  34. Examples:
  35. touch("hello_world.py")
  36. """
  37. try:
  38. with open(file_name, "a"):
  39. os.utime(file_name, times)
  40. except OSError as e: # pragma: no cover
  41. log.warn(
  42. "Failed to create lock, do not run conda in parallel processes [errno %d]",
  43. e.errno,
  44. )
  45. class FileLock:
  46. """Lock a path (file or directory) with the lock file sitting *beside* path.
  47. :param path_to_lock: the path to be locked
  48. :param retries: max number of retries
  49. """
  50. def __init__(self, path_to_lock, retries=10):
  51. self.path_to_lock = abspath(path_to_lock)
  52. self.retries = retries
  53. self.lock_file_path = f"{self.path_to_lock}.pid{{0}}.{LOCK_EXTENSION}"
  54. # e.g. if locking path `/conda`, lock file will be `/conda.pidXXXX.conda_lock`
  55. self.lock_file_glob_str = f"{self.path_to_lock}.pid*.{LOCK_EXTENSION}"
  56. assert isdir(dirname(self.path_to_lock)), f"{self.path_to_lock} doesn't exist"
  57. assert "::" not in self.path_to_lock, self.path_to_lock
  58. def __enter__(self):
  59. sleep_time = 1
  60. self.lock_file_path = self.lock_file_path.format(os.getpid())
  61. last_glob_match = None
  62. for _ in range(self.retries + 1):
  63. # search, whether there is process already locked on this file
  64. glob_result = glob(self.lock_file_glob_str)
  65. if glob_result:
  66. log.debug(LOCKSTR.format(glob_result))
  67. log.debug("Sleeping for %s seconds", sleep_time)
  68. time.sleep(sleep_time / 10)
  69. sleep_time *= 2
  70. last_glob_match = glob_result
  71. else:
  72. touch(self.lock_file_path)
  73. return self
  74. stdoutlog.error("Exceeded max retries, giving up")
  75. raise LockError(LOCKSTR.format(last_glob_match))
  76. def __exit__(self, exc_type, exc_value, traceback):
  77. from .gateways.disk.delete import rm_rf
  78. rm_rf(self.lock_file_path)
  79. # lgtm alert ignore because this lock functionality is unused and will soon be replaced
  80. class DirectoryLock(FileLock): # lgtm [py/missing-call-to-init]
  81. """Lock a directory with the lock file sitting *within* the directory being locked.
  82. Useful when, for example, locking the root prefix at ``/conda``, and ``/`` is not writable.
  83. :param directory_path: the path to be locked
  84. :param retries: max number of retries
  85. """
  86. def __init__(self, directory_path, retries=10):
  87. self.directory_path = abspath(directory_path)
  88. directory_name = basename(self.directory_path)
  89. self.retries = retries
  90. lock_path_pre = join(self.directory_path, directory_name)
  91. self.lock_file_path = f"{lock_path_pre}.pid{{0}}.{LOCK_EXTENSION}"
  92. # e.g. if locking directory `/conda`, lock file will be `/conda/conda.pidXXXX.conda_lock`
  93. self.lock_file_glob_str = f"{lock_path_pre}.pid*.{LOCK_EXTENSION}"
  94. # make sure '/' exists
  95. assert isdir(
  96. dirname(self.directory_path)
  97. ), f"{self.directory_path} doesn't exist"
  98. if not isdir(self.directory_path):
  99. try:
  100. os.makedirs(self.directory_path)
  101. log.debug("forced to create %s", self.directory_path)
  102. except OSError as e: # pragma: no cover
  103. log.warn(
  104. "Failed to create directory %s [errno %d]",
  105. self.directory_path,
  106. e.errno,
  107. )
  108. Locked = DirectoryLock