lock.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2014 Jean-Marc Martins
  3. # Copyright © 2012-2017 Guillaume Ayoub
  4. # Copyright © 2017-2022 Unrud <unrud@outlook.com>
  5. # Copyright © 2023-2025 Peter Bieringer <pb@bieringer.de>
  6. #
  7. # This library is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  19. import contextlib
  20. import logging
  21. import os
  22. import shlex
  23. import signal
  24. import subprocess
  25. import sys
  26. from typing import Iterator
  27. from radicale import config, pathutils, types
  28. from radicale.log import logger
  29. from radicale.storage.multifilesystem.base import CollectionBase, StorageBase
  30. class CollectionPartLock(CollectionBase):
  31. @types.contextmanager
  32. def _acquire_cache_lock(self, ns: str = "") -> Iterator[None]:
  33. if self._storage._lock.locked == "w":
  34. yield
  35. return
  36. cache_folder = self._storage._get_collection_cache_subfolder(self._filesystem_path, ".Radicale.cache", ns)
  37. self._storage._makedirs_synced(cache_folder)
  38. lock_path = os.path.join(cache_folder,
  39. ".Radicale.lock" + (".%s" % ns if ns else ""))
  40. logger.debug("Lock file (CollectionPartLock): %r" % lock_path)
  41. lock = pathutils.RwLock(lock_path)
  42. with lock.acquire("w"):
  43. yield
  44. class StoragePartLock(StorageBase):
  45. _lock: pathutils.RwLock
  46. _hook: str
  47. def __init__(self, configuration: config.Configuration) -> None:
  48. super().__init__(configuration)
  49. lock_path = os.path.join(self._filesystem_folder, ".Radicale.lock")
  50. logger.debug("Lock file (StoragePartLock): %r" % lock_path)
  51. self._lock = pathutils.RwLock(lock_path)
  52. self._hook = configuration.get("storage", "hook")
  53. @types.contextmanager
  54. def acquire_lock(self, mode: str, user: str = "", *args, **kwargs) -> Iterator[None]:
  55. with self._lock.acquire(mode):
  56. yield
  57. # execute hook
  58. if mode == "w" and self._hook:
  59. debug = logger.isEnabledFor(logging.DEBUG)
  60. # Use new process group for child to prevent terminals
  61. # from sending SIGINT etc.
  62. preexec_fn = None
  63. creationflags = 0
  64. if sys.platform == "win32":
  65. creationflags |= subprocess.CREATE_NEW_PROCESS_GROUP
  66. else:
  67. # Process group is also used to identify child processes
  68. preexec_fn = os.setpgrp
  69. # optional argument
  70. path = kwargs.get('path', "")
  71. request = kwargs.get('request', "NONE")
  72. to_path = kwargs.get('to_path', "")
  73. if to_path != "":
  74. to_path = shlex.quote(self._get_collection_root_folder() + to_path)
  75. try:
  76. command = self._hook % {
  77. "path": shlex.quote(self._get_collection_root_folder() + path),
  78. "to_path": to_path,
  79. "cwd": shlex.quote(self._filesystem_folder),
  80. "request": shlex.quote(request),
  81. "user": shlex.quote(user or "Anonymous")}
  82. except KeyError as e:
  83. logger.error("Storage hook contains not supported placeholder %s (skip execution of: %r)" % (e, self._hook))
  84. return
  85. logger.debug("Executing storage hook: '%s'" % command)
  86. try:
  87. p = subprocess.Popen(
  88. command, stdin=subprocess.DEVNULL,
  89. stdout=subprocess.PIPE if debug else subprocess.DEVNULL,
  90. stderr=subprocess.PIPE if debug else subprocess.DEVNULL,
  91. shell=True, universal_newlines=True, preexec_fn=preexec_fn,
  92. cwd=self._filesystem_folder, creationflags=creationflags)
  93. except Exception as e:
  94. logger.error("Execution of storage hook not successful on 'Popen': %s" % e)
  95. return
  96. logger.debug("Executing storage hook started 'Popen'")
  97. try:
  98. stdout_data, stderr_data = p.communicate()
  99. except BaseException as e: # e.g. KeyboardInterrupt or SystemExit
  100. logger.error("Execution of storage hook not successful on 'communicate': %s" % e)
  101. p.kill()
  102. p.wait()
  103. return
  104. finally:
  105. if sys.platform != "win32":
  106. # Kill remaining children identified by process group
  107. with contextlib.suppress(OSError):
  108. os.killpg(p.pid, signal.SIGKILL)
  109. logger.debug("Executing storage hook finished")
  110. if stdout_data:
  111. logger.debug("Captured stdout from storage hook:\n%s", stdout_data)
  112. if stderr_data:
  113. logger.debug("Captured stderr from storage hook:\n%s", stderr_data)
  114. if p.returncode != 0:
  115. logger.error("Execution of storage hook not successful: %s" % subprocess.CalledProcessError(p.returncode, p.args))
  116. return