1
0

storage.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2014 Jean-Marc Martins
  3. # Copyright © 2012-2016 Guillaume Ayoub
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Storage backends.
  19. This module loads the storage backend, according to the storage configuration.
  20. Default storage uses one folder per collection and one file per collection
  21. entry.
  22. """
  23. import errno
  24. import json
  25. import os
  26. import posixpath
  27. import shutil
  28. import stat
  29. import threading
  30. import time
  31. from contextlib import contextmanager
  32. from hashlib import md5
  33. from importlib import import_module
  34. from itertools import groupby
  35. from random import getrandbits
  36. from tempfile import TemporaryDirectory
  37. from atomicwrites import AtomicWriter
  38. import vobject
  39. if os.name == "nt":
  40. import ctypes
  41. import ctypes.wintypes
  42. import msvcrt
  43. LOCKFILE_EXCLUSIVE_LOCK = 2
  44. if ctypes.sizeof(ctypes.c_void_p) == 4:
  45. ULONG_PTR = ctypes.c_uint32
  46. else:
  47. ULONG_PTR = ctypes.c_uint64
  48. class Overlapped(ctypes.Structure):
  49. _fields_ = [("internal", ULONG_PTR),
  50. ("internal_high", ULONG_PTR),
  51. ("offset", ctypes.wintypes.DWORD),
  52. ("offset_high", ctypes.wintypes.DWORD),
  53. ("h_event", ctypes.wintypes.HANDLE)]
  54. lock_file_ex = ctypes.windll.kernel32.LockFileEx
  55. lock_file_ex.argtypes = [ctypes.wintypes.HANDLE,
  56. ctypes.wintypes.DWORD,
  57. ctypes.wintypes.DWORD,
  58. ctypes.wintypes.DWORD,
  59. ctypes.wintypes.DWORD,
  60. ctypes.POINTER(Overlapped)]
  61. lock_file_ex.restype = ctypes.wintypes.BOOL
  62. unlock_file_ex = ctypes.windll.kernel32.UnlockFileEx
  63. unlock_file_ex.argtypes = [ctypes.wintypes.HANDLE,
  64. ctypes.wintypes.DWORD,
  65. ctypes.wintypes.DWORD,
  66. ctypes.wintypes.DWORD,
  67. ctypes.POINTER(Overlapped)]
  68. unlock_file_ex.restype = ctypes.wintypes.BOOL
  69. elif os.name == "posix":
  70. import fcntl
  71. def load(configuration, logger):
  72. """Load the storage manager chosen in configuration."""
  73. storage_type = configuration.get("storage", "type")
  74. if storage_type == "multifilesystem":
  75. collection_class = Collection
  76. else:
  77. collection_class = import_module(storage_type).Collection
  78. class CollectionCopy(collection_class):
  79. """Collection copy, avoids overriding the original class attributes."""
  80. CollectionCopy.configuration = configuration
  81. CollectionCopy.logger = logger
  82. return CollectionCopy
  83. MIMETYPES = {"VADDRESSBOOK": "text/vcard", "VCALENDAR": "text/calendar"}
  84. def get_etag(text):
  85. """Etag from collection or item."""
  86. etag = md5()
  87. etag.update(text.encode("utf-8"))
  88. return '"%s"' % etag.hexdigest()
  89. def is_safe_path_component(path):
  90. """Check if path is a single component of a path.
  91. Check that the path is safe to join too.
  92. """
  93. return path and "/" not in path and path not in (".", "..")
  94. def sanitize_path(path):
  95. """Make path absolute with leading slash to prevent access to other data.
  96. Preserve a potential trailing slash.
  97. """
  98. trailing_slash = "/" if path.endswith("/") else ""
  99. path = posixpath.normpath(path)
  100. new_path = "/"
  101. for part in path.split("/"):
  102. if not part or part in (".", ".."):
  103. continue
  104. new_path = posixpath.join(new_path, part)
  105. trailing_slash = "" if new_path.endswith("/") else trailing_slash
  106. return new_path + trailing_slash
  107. def is_safe_filesystem_path_component(path):
  108. """Check if path is a single component of a filesystem path.
  109. Check that the path is safe to join too.
  110. """
  111. return (
  112. path and not os.path.splitdrive(path)[0] and
  113. not os.path.split(path)[0] and path not in (os.curdir, os.pardir) and
  114. not path.startswith(".") and not path.endswith("~"))
  115. def path_to_filesystem(root, *paths):
  116. """Convert path to a local filesystem path relative to base_folder.
  117. `root` must be a secure filesystem path, it will be prepend to the path.
  118. Conversion of `paths` is done in a secure manner, or raises ``ValueError``.
  119. """
  120. paths = [sanitize_path(path).strip("/") for path in paths]
  121. safe_path = root
  122. for path in paths:
  123. if not path:
  124. continue
  125. for part in path.split("/"):
  126. if not is_safe_filesystem_path_component(part):
  127. raise ValueError(
  128. "Can't tranlate name safely to filesystem: %s" % part)
  129. safe_path = os.path.join(safe_path, part)
  130. return safe_path
  131. def sync_directory(path):
  132. """Sync directory to disk
  133. This only works on POSIX and does nothing on other systems.
  134. """
  135. if os.name == "posix":
  136. fd = os.open(path, 0)
  137. try:
  138. if hasattr(fcntl, "F_FULLFSYNC"):
  139. fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
  140. else:
  141. os.fsync(fd)
  142. finally:
  143. os.close(fd)
  144. class _EncodedAtomicWriter(AtomicWriter):
  145. def __init__(self, path, encoding, mode="w", overwrite=True):
  146. self._encoding = encoding
  147. return super().__init__(path, mode, overwrite=True)
  148. def get_fileobject(self, **kwargs):
  149. return super().get_fileobject(encoding=self._encoding,
  150. prefix=".Radicale.tmp-", **kwargs)
  151. class Item:
  152. def __init__(self, collection, item, href, last_modified=None):
  153. self.collection = collection
  154. self.item = item
  155. self.href = href
  156. self.last_modified = last_modified
  157. def __getattr__(self, attr):
  158. return getattr(self.item, attr)
  159. @property
  160. def etag(self):
  161. return get_etag(self.serialize())
  162. class BaseCollection:
  163. # Overriden on copy by the "load" function
  164. configuration = None
  165. logger = None
  166. def __init__(self, path, principal=False):
  167. """Initialize the collection.
  168. ``path`` must be the normalized relative path of the collection, using
  169. the slash as the folder delimiter, with no leading nor trailing slash.
  170. """
  171. raise NotImplementedError
  172. @classmethod
  173. def discover(cls, path, depth="1"):
  174. """Discover a list of collections under the given ``path``.
  175. If ``depth`` is "0", only the actual object under ``path`` is
  176. returned.
  177. If ``depth`` is anything but "0", it is considered as "1" and direct
  178. children are included in the result.
  179. The ``path`` is relative.
  180. """
  181. raise NotImplementedError
  182. @property
  183. def etag(self):
  184. return get_etag(self.serialize())
  185. @classmethod
  186. def create_collection(cls, href, collection=None, props=None):
  187. """Create a collection.
  188. ``collection`` is a list of vobject components.
  189. ``props`` are metadata values for the collection.
  190. ``props["tag"]`` is the type of collection (VCALENDAR or VADDRESSBOOK). If
  191. the key ``tag`` is missing, it is guessed from the collection.
  192. """
  193. raise NotImplementedError
  194. def list(self):
  195. """List collection items."""
  196. raise NotImplementedError
  197. def get(self, href):
  198. """Fetch a single item."""
  199. raise NotImplementedError
  200. def get_multi(self, hrefs):
  201. """Fetch multiple items. Duplicate hrefs must be ignored.
  202. Functionally similar to ``get``, but might bring performance benefits
  203. on some storages when used cleverly.
  204. """
  205. for href in set(hrefs):
  206. yield self.get(href)
  207. def pre_filtered_list(self, filters):
  208. """List collection items with optional pre filtering.
  209. This could largely improve performance of reports depending on
  210. the filters and this implementation.
  211. This returns all event by default
  212. """
  213. return [self.get(href) for href, _ in self.list()]
  214. def has(self, href):
  215. """Check if an item exists by its href.
  216. Functionally similar to ``get``, but might bring performance benefits
  217. on some storages when used cleverly.
  218. """
  219. return self.get(href) is not None
  220. def upload(self, href, vobject_item):
  221. """Upload a new item."""
  222. raise NotImplementedError
  223. def update(self, href, vobject_item, etag=None):
  224. """Update an item.
  225. Functionally similar to ``delete`` plus ``upload``, but might bring
  226. performance benefits on some storages when used cleverly.
  227. """
  228. self.delete(href, etag)
  229. self.upload(href, vobject_item)
  230. def delete(self, href=None, etag=None):
  231. """Delete an item.
  232. When ``href`` is ``None``, delete the collection.
  233. """
  234. raise NotImplementedError
  235. def get_meta(self, key):
  236. """Get metadata value for collection."""
  237. raise NotImplementedError
  238. def set_meta(self, props):
  239. """Set metadata values for collection."""
  240. raise NotImplementedError
  241. @property
  242. def last_modified(self):
  243. """Get the HTTP-datetime of when the collection was modified."""
  244. raise NotImplementedError
  245. def serialize(self):
  246. """Get the unicode string representing the whole collection."""
  247. raise NotImplementedError
  248. @classmethod
  249. @contextmanager
  250. def acquire_lock(cls, mode):
  251. """Set a context manager to lock the whole storage.
  252. ``mode`` must either be "r" for shared access or "w" for exclusive
  253. access.
  254. """
  255. raise NotImplementedError
  256. class Collection(BaseCollection):
  257. """Collection stored in several files per calendar."""
  258. def __init__(self, path, principal=False, folder=None):
  259. if not folder:
  260. folder = self._get_collection_root_folder()
  261. # path should already be sanitized
  262. self.path = sanitize_path(path).strip("/")
  263. self.storage_encoding = self.configuration.get("encoding", "stock")
  264. self._filesystem_path = path_to_filesystem(folder, self.path)
  265. self._props_path = os.path.join(
  266. self._filesystem_path, ".Radicale.props")
  267. split_path = self.path.split("/")
  268. if len(split_path) > 1:
  269. # URL with at least one folder
  270. self.owner = split_path[0]
  271. else:
  272. self.owner = None
  273. self.is_principal = principal
  274. @classmethod
  275. def _get_collection_root_folder(cls):
  276. filesystem_folder = os.path.expanduser(
  277. cls.configuration.get("storage", "filesystem_folder"))
  278. folder = os.path.join(filesystem_folder, "collection-root")
  279. return folder
  280. @contextmanager
  281. def _atomic_write(self, path, mode="w"):
  282. with _EncodedAtomicWriter(
  283. path, self.storage_encoding, mode).open() as fd:
  284. yield fd
  285. def _find_available_file_name(self):
  286. # Prevent infinite loop
  287. for _ in range(10000):
  288. file_name = hex(getrandbits(32))[2:]
  289. if not self.has(file_name):
  290. return file_name
  291. raise FileExistsError(errno.EEXIST, "No usable file name found")
  292. @classmethod
  293. def discover(cls, path, depth="1"):
  294. # path == None means wrong URL
  295. if path is None:
  296. return
  297. # path should already be sanitized
  298. sane_path = sanitize_path(path).strip("/")
  299. attributes = sane_path.split("/")
  300. if not attributes[0]:
  301. attributes.pop()
  302. # Try to guess if the path leads to a collection or an item
  303. folder = cls._get_collection_root_folder()
  304. try:
  305. filesystem_path = path_to_filesystem(folder, sane_path)
  306. except ValueError:
  307. # Path is unsafe
  308. return
  309. href = None
  310. if not os.path.isdir(filesystem_path):
  311. if attributes and os.path.isfile(filesystem_path):
  312. href = attributes.pop()
  313. else:
  314. return
  315. path = "/".join(attributes)
  316. principal = len(attributes) == 1
  317. collection = cls(path, principal)
  318. if href:
  319. yield collection.get(href)
  320. return
  321. yield collection
  322. if depth == "0":
  323. return
  324. for item in collection.list():
  325. yield collection.get(item[0])
  326. for href in os.listdir(filesystem_path):
  327. if not is_safe_filesystem_path_component(href):
  328. cls.logger.debug("Skipping collection: %s", href)
  329. continue
  330. child_filesystem_path = path_to_filesystem(filesystem_path, href)
  331. if os.path.isdir(child_filesystem_path):
  332. child_principal = len(attributes) == 0
  333. yield cls(child_filesystem_path, child_principal)
  334. @classmethod
  335. def create_collection(cls, href, collection=None, props=None):
  336. folder = cls._get_collection_root_folder()
  337. # path should already be sanitized
  338. sane_path = sanitize_path(href).strip("/")
  339. attributes = sane_path.split("/")
  340. if not attributes[0]:
  341. attributes.pop()
  342. principal = len(attributes) == 1
  343. filesystem_path = path_to_filesystem(folder, sane_path)
  344. if not props:
  345. props = {}
  346. if not props.get("tag") and collection:
  347. props["tag"] = collection[0].name
  348. if not props:
  349. os.makedirs(filesystem_path, exist_ok=True)
  350. return cls(sane_path, principal=principal)
  351. parent_dir = os.path.dirname(filesystem_path)
  352. os.makedirs(parent_dir, exist_ok=True)
  353. with TemporaryDirectory(prefix=".Radicale.tmp-",
  354. dir=parent_dir) as tmp_dir:
  355. # The temporary directory itself can't be renamed
  356. tmp_filesystem_path = os.path.join(tmp_dir, "collection")
  357. os.makedirs(tmp_filesystem_path)
  358. # path is unsafe
  359. self = cls("/", principal=principal, folder=tmp_filesystem_path)
  360. self.set_meta(props)
  361. if props.get("tag") == "VCALENDAR":
  362. if collection:
  363. collection, = collection
  364. items = []
  365. for content in ("vevent", "vtodo", "vjournal"):
  366. items.extend(getattr(collection, "%s_list" % content,
  367. []))
  368. def get_uid(item):
  369. return hasattr(item, "uid") and item.uid.value
  370. items_by_uid = groupby(
  371. sorted(items, key=get_uid), get_uid)
  372. for uid, items in items_by_uid:
  373. new_collection = vobject.iCalendar()
  374. for item in items:
  375. new_collection.add(item)
  376. self.upload(
  377. self._find_available_file_name(), new_collection)
  378. elif props.get("tag") == "VCARD":
  379. if collection:
  380. for card in collection:
  381. self.upload(self._find_available_file_name(), card)
  382. os.rename(tmp_filesystem_path, filesystem_path)
  383. sync_directory(parent_dir)
  384. return cls(sane_path, principal=principal)
  385. def list(self):
  386. try:
  387. hrefs = os.listdir(self._filesystem_path)
  388. except IOError:
  389. return
  390. for href in hrefs:
  391. if not is_safe_filesystem_path_component(href):
  392. self.logger.debug("Skipping component: %s", href)
  393. continue
  394. path = os.path.join(self._filesystem_path, href)
  395. if os.path.isfile(path):
  396. with open(path, encoding=self.storage_encoding) as fd:
  397. yield href, get_etag(fd.read())
  398. def get(self, href):
  399. if not href:
  400. return None
  401. href = href.strip("{}").replace("/", "_")
  402. if not is_safe_filesystem_path_component(href):
  403. self.logger.debug(
  404. "Can't tranlate name safely to filesystem: %s", href)
  405. return None
  406. path = path_to_filesystem(self._filesystem_path, href)
  407. if not os.path.isfile(path):
  408. return None
  409. with open(path, encoding=self.storage_encoding) as fd:
  410. text = fd.read()
  411. last_modified = time.strftime(
  412. "%a, %d %b %Y %H:%M:%S GMT",
  413. time.gmtime(os.path.getmtime(path)))
  414. return Item(self, vobject.readOne(text), href, last_modified)
  415. def has(self, href):
  416. return self.get(href) is not None
  417. def upload(self, href, vobject_item):
  418. # TODO: use returned object in code
  419. if not is_safe_filesystem_path_component(href):
  420. raise ValueError(
  421. "Can't tranlate name safely to filesystem: %s" % href)
  422. path = path_to_filesystem(self._filesystem_path, href)
  423. if os.path.exists(path):
  424. raise ValueError("Component already exists: %s" % href)
  425. item = Item(self, vobject_item, href)
  426. with self._atomic_write(path) as fd:
  427. fd.write(item.serialize())
  428. return item
  429. def update(self, href, vobject_item, etag=None):
  430. # TODO: use etag in code and test it here
  431. # TODO: use returned object in code
  432. if not is_safe_filesystem_path_component(href):
  433. raise ValueError(
  434. "Can't tranlate name safely to filesystem: %s" % href)
  435. path = path_to_filesystem(self._filesystem_path, href)
  436. if not os.path.isfile(path):
  437. raise ValueError("Component doesn't exist: %s" % href)
  438. with open(path, encoding=self.storage_encoding) as fd:
  439. text = fd.read()
  440. if etag and etag != get_etag(text):
  441. raise ValueError(
  442. "ETag doesn't match: %s != %s" % (etag, get_etag(text)))
  443. item = Item(self, vobject_item, href)
  444. with self._atomic_write(path) as fd:
  445. fd.write(item.serialize())
  446. return item
  447. def delete(self, href=None, etag=None):
  448. # TODO: use etag in code and test it here
  449. # TODO: use returned object in code
  450. if href is None:
  451. # Delete the collection
  452. if os.path.isdir(self._filesystem_path):
  453. shutil.rmtree(self._filesystem_path)
  454. else:
  455. # Delete an item
  456. if not is_safe_filesystem_path_component(href):
  457. raise ValueError(
  458. "Can't tranlate name safely to filesystem: %s" % href)
  459. path = path_to_filesystem(self._filesystem_path, href)
  460. if not os.path.isfile(path):
  461. raise ValueError("Component doesn't exist: %s" % href)
  462. with open(path, encoding=self.storage_encoding) as fd:
  463. text = fd.read()
  464. if etag and etag != get_etag(text):
  465. raise ValueError(
  466. "ETag doesn't match: %s != %s" % (etag, get_etag(text)))
  467. os.remove(path)
  468. def get_meta(self, key):
  469. if os.path.exists(self._props_path):
  470. with open(self._props_path, encoding=self.storage_encoding) as prop:
  471. return json.load(prop).get(key)
  472. def set_meta(self, props):
  473. if os.path.exists(self._props_path):
  474. with open(self._props_path, encoding=self.storage_encoding) as prop:
  475. old_props = json.load(prop)
  476. old_props.update(props)
  477. props = old_props
  478. # filter empty entries
  479. props = {k:v for k,v in props.items() if v}
  480. with self._atomic_write(self._props_path, "w+") as prop:
  481. json.dump(props, prop)
  482. @property
  483. def last_modified(self):
  484. last = max([os.path.getmtime(self._filesystem_path)] + [
  485. os.path.getmtime(os.path.join(self._filesystem_path, filename))
  486. for filename in os.listdir(self._filesystem_path)] or [0])
  487. return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last))
  488. def serialize(self):
  489. if not os.path.exists(self._filesystem_path):
  490. return None
  491. items = []
  492. for href in os.listdir(self._filesystem_path):
  493. if not is_safe_filesystem_path_component(href):
  494. self.logger.debug("Skipping component: %s", href)
  495. continue
  496. path = os.path.join(self._filesystem_path, href)
  497. if os.path.isfile(path):
  498. with open(path, encoding=self.storage_encoding) as fd:
  499. items.append(vobject.readOne(fd.read()))
  500. if self.get_meta("tag") == "VCALENDAR":
  501. collection = vobject.iCalendar()
  502. for item in items:
  503. for content in ("vevent", "vtodo", "vjournal"):
  504. if content in item.contents:
  505. for item_part in getattr(item, "%s_list" % content):
  506. collection.add(item_part)
  507. break
  508. return collection.serialize()
  509. elif self.get_meta("tag") == "VADDRESSBOOK":
  510. return "".join([item.serialize() for item in items])
  511. return ""
  512. _lock = threading.Lock()
  513. _waiters = []
  514. _lock_file = None
  515. _lock_file_locked = False
  516. _readers = 0
  517. _writer = False
  518. @classmethod
  519. @contextmanager
  520. def acquire_lock(cls, mode):
  521. def condition():
  522. if mode == "r":
  523. return not cls._writer
  524. else:
  525. return not cls._writer and cls._readers == 0
  526. if mode not in ("r", "w"):
  527. raise ValueError("Invalid lock mode: %s" % mode)
  528. # Use a primitive lock which only works within one process as a
  529. # precondition for inter-process file-based locking
  530. with cls._lock:
  531. if cls._waiters or not condition():
  532. # use FIFO for access requests
  533. waiter = threading.Condition(lock=cls._lock)
  534. cls._waiters.append(waiter)
  535. while True:
  536. waiter.wait()
  537. if condition():
  538. break
  539. cls._waiters.pop(0)
  540. if mode == "r":
  541. cls._readers += 1
  542. # notify additional potential readers
  543. if cls._waiters:
  544. cls._waiters[0].notify()
  545. else:
  546. cls._writer = True
  547. if not cls._lock_file:
  548. folder = os.path.expanduser(
  549. cls.configuration.get("storage", "filesystem_folder"))
  550. if not os.path.exists(folder):
  551. os.makedirs(folder, exist_ok=True)
  552. lock_path = os.path.join(folder, ".Radicale.lock")
  553. cls._lock_file = open(lock_path, "w+")
  554. # set access rights to a necessary minimum to prevent locking
  555. # by arbitrary users
  556. try:
  557. os.chmod(lock_path, stat.S_IWUSR | stat.S_IRUSR)
  558. except OSError:
  559. cls.logger.debug("Failed to set permissions on lock file")
  560. if not cls._lock_file_locked:
  561. if os.name == "nt":
  562. handle = msvcrt.get_osfhandle(cls._lock_file.fileno())
  563. flags = LOCKFILE_EXCLUSIVE_LOCK if mode == "w" else 0
  564. overlapped = Overlapped()
  565. if not lock_file_ex(handle, flags, 0, 1, 0, overlapped):
  566. cls.logger.debug("Locking not supported")
  567. elif os.name == "posix":
  568. _cmd = fcntl.LOCK_EX if mode == "w" else fcntl.LOCK_SH
  569. try:
  570. fcntl.flock(cls._lock_file.fileno(), _cmd)
  571. except OSError:
  572. cls.logger.debug("Locking not supported")
  573. cls._lock_file_locked = True
  574. try:
  575. yield
  576. finally:
  577. with cls._lock:
  578. if mode == "r":
  579. cls._readers -= 1
  580. else:
  581. cls._writer = False
  582. if cls._readers == 0:
  583. if os.name == "nt":
  584. handle = msvcrt.get_osfhandle(cls._lock_file.fileno())
  585. overlapped = Overlapped()
  586. if not unlock_file_ex(handle, 0, 1, 0, overlapped):
  587. cls.logger.debug("Unlocking not supported")
  588. elif os.name == "posix":
  589. try:
  590. fcntl.flock(cls._lock_file.fileno(), fcntl.LOCK_UN)
  591. except OSError:
  592. cls.logger.debug("Unlocking not supported")
  593. cls._lock_file_locked = False
  594. if cls._waiters:
  595. cls._waiters[0].notify()