PageRenderTime 47ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/hgext/inotify/linux/watcher.py

https://bitbucket.org/mirror/mercurial/
Python | 335 lines | 219 code | 42 blank | 74 comment | 9 complexity | de5d807dbc9948c257dad97f4e508264 MD5 | raw file
Possible License(s): GPL-2.0
  1. # watcher.py - high-level interfaces to the Linux inotify subsystem
  2. # Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
  3. # This library is free software; you can redistribute it and/or modify
  4. # it under the terms of version 2.1 of the GNU Lesser General Public
  5. # License, or any later version.
  6. '''High-level interfaces to the Linux inotify subsystem.
  7. The inotify subsystem provides an efficient mechanism for file status
  8. monitoring and change notification.
  9. The watcher class hides the low-level details of the inotify
  10. interface, and provides a Pythonic wrapper around it. It generates
  11. events that provide somewhat more information than raw inotify makes
  12. available.
  13. The autowatcher class is more useful, as it automatically watches
  14. newly-created directories on your behalf.'''
  15. __author__ = "Bryan O'Sullivan <bos@serpentine.com>"
  16. import _inotify as inotify
  17. import array
  18. import errno
  19. import fcntl
  20. import os
  21. import termios
  22. class event(object):
  23. '''Derived inotify event class.
  24. The following fields are available:
  25. mask: event mask, indicating what kind of event this is
  26. cookie: rename cookie, if a rename-related event
  27. path: path of the directory in which the event occurred
  28. name: name of the directory entry to which the event occurred
  29. (may be None if the event happened to a watched directory)
  30. fullpath: complete path at which the event occurred
  31. wd: watch descriptor that triggered this event'''
  32. __slots__ = (
  33. 'cookie',
  34. 'fullpath',
  35. 'mask',
  36. 'name',
  37. 'path',
  38. 'raw',
  39. 'wd',
  40. )
  41. def __init__(self, raw, path):
  42. self.path = path
  43. self.raw = raw
  44. if raw.name:
  45. self.fullpath = path + '/' + raw.name
  46. else:
  47. self.fullpath = path
  48. self.wd = raw.wd
  49. self.mask = raw.mask
  50. self.cookie = raw.cookie
  51. self.name = raw.name
  52. def __repr__(self):
  53. r = repr(self.raw)
  54. return 'event(path=' + repr(self.path) + ', ' + r[r.find('(')+1:]
  55. _event_props = {
  56. 'access': 'File was accessed',
  57. 'modify': 'File was modified',
  58. 'attrib': 'Attribute of a directory entry was changed',
  59. 'close_write': 'File was closed after being written to',
  60. 'close_nowrite': 'File was closed without being written to',
  61. 'open': 'File was opened',
  62. 'moved_from': 'Directory entry was renamed from this name',
  63. 'moved_to': 'Directory entry was renamed to this name',
  64. 'create': 'Directory entry was created',
  65. 'delete': 'Directory entry was deleted',
  66. 'delete_self': 'The watched directory entry was deleted',
  67. 'move_self': 'The watched directory entry was renamed',
  68. 'unmount': 'Directory was unmounted, and can no longer be watched',
  69. 'q_overflow': 'Kernel dropped events due to queue overflow',
  70. 'ignored': 'Directory entry is no longer being watched',
  71. 'isdir': 'Event occurred on a directory',
  72. }
  73. for k, v in _event_props.iteritems():
  74. mask = getattr(inotify, 'IN_' + k.upper())
  75. def getter(self):
  76. return self.mask & mask
  77. getter.__name__ = k
  78. getter.__doc__ = v
  79. setattr(event, k, property(getter, doc=v))
  80. del _event_props
  81. class watcher(object):
  82. '''Provide a Pythonic interface to the low-level inotify API.
  83. Also adds derived information to each event that is not available
  84. through the normal inotify API, such as directory name.'''
  85. __slots__ = (
  86. 'fd',
  87. '_paths',
  88. '_wds',
  89. )
  90. def __init__(self):
  91. '''Create a new inotify instance.'''
  92. self.fd = inotify.init()
  93. self._paths = {}
  94. self._wds = {}
  95. def fileno(self):
  96. '''Return the file descriptor this watcher uses.
  97. Useful for passing to select and poll.'''
  98. return self.fd
  99. def add(self, path, mask):
  100. '''Add or modify a watch.
  101. Return the watch descriptor added or modified.'''
  102. path = os.path.normpath(path)
  103. wd = inotify.add_watch(self.fd, path, mask)
  104. self._paths[path] = wd, mask
  105. self._wds[wd] = path, mask
  106. return wd
  107. def remove(self, wd):
  108. '''Remove the given watch.'''
  109. inotify.remove_watch(self.fd, wd)
  110. self._remove(wd)
  111. def _remove(self, wd):
  112. path_mask = self._wds.pop(wd, None)
  113. if path_mask is not None:
  114. self._paths.pop(path_mask[0])
  115. def path(self, path):
  116. '''Return a (watch descriptor, event mask) pair for the given path.
  117. If the path is not being watched, return None.'''
  118. return self._paths.get(path)
  119. def wd(self, wd):
  120. '''Return a (path, event mask) pair for the given watch descriptor.
  121. If the watch descriptor is not valid or not associated with
  122. this watcher, return None.'''
  123. return self._wds.get(wd)
  124. def read(self, bufsize=None):
  125. '''Read a list of queued inotify events.
  126. If bufsize is zero, only return those events that can be read
  127. immediately without blocking. Otherwise, block until events are
  128. available.'''
  129. events = []
  130. for evt in inotify.read(self.fd, bufsize):
  131. events.append(event(evt, self._wds[evt.wd][0]))
  132. if evt.mask & inotify.IN_IGNORED:
  133. self._remove(evt.wd)
  134. elif evt.mask & inotify.IN_UNMOUNT:
  135. self.close()
  136. return events
  137. def close(self):
  138. '''Shut down this watcher.
  139. All subsequent method calls are likely to raise exceptions.'''
  140. os.close(self.fd)
  141. self.fd = None
  142. self._paths = None
  143. self._wds = None
  144. def __len__(self):
  145. '''Return the number of active watches.'''
  146. return len(self._paths)
  147. def __iter__(self):
  148. '''Yield a (path, watch descriptor, event mask) tuple for each
  149. entry being watched.'''
  150. for path, (wd, mask) in self._paths.iteritems():
  151. yield path, wd, mask
  152. def __del__(self):
  153. if self.fd is not None:
  154. os.close(self.fd)
  155. ignored_errors = [errno.ENOENT, errno.EPERM, errno.ENOTDIR]
  156. def add_iter(self, path, mask, onerror=None):
  157. '''Add or modify watches over path and its subdirectories.
  158. Yield each added or modified watch descriptor.
  159. To ensure that this method runs to completion, you must
  160. iterate over all of its results, even if you do not care what
  161. they are. For example:
  162. for wd in w.add_iter(path, mask):
  163. pass
  164. By default, errors are ignored. If optional arg "onerror" is
  165. specified, it should be a function; it will be called with one
  166. argument, an OSError instance. It can report the error to
  167. continue with the walk, or raise the exception to abort the
  168. walk.'''
  169. # Add the IN_ONLYDIR flag to the event mask, to avoid a possible
  170. # race when adding a subdirectory. In the time between the
  171. # event being queued by the kernel and us processing it, the
  172. # directory may have been deleted, or replaced with a different
  173. # kind of entry with the same name.
  174. submask = mask | inotify.IN_ONLYDIR
  175. try:
  176. yield self.add(path, mask)
  177. except OSError, err:
  178. if onerror and err.errno not in self.ignored_errors:
  179. onerror(err)
  180. for root, dirs, names in os.walk(path, topdown=False, onerror=onerror):
  181. for d in dirs:
  182. try:
  183. yield self.add(root + '/' + d, submask)
  184. except OSError, err:
  185. if onerror and err.errno not in self.ignored_errors:
  186. onerror(err)
  187. def add_all(self, path, mask, onerror=None):
  188. '''Add or modify watches over path and its subdirectories.
  189. Return a list of added or modified watch descriptors.
  190. By default, errors are ignored. If optional arg "onerror" is
  191. specified, it should be a function; it will be called with one
  192. argument, an OSError instance. It can report the error to
  193. continue with the walk, or raise the exception to abort the
  194. walk.'''
  195. return [w for w in self.add_iter(path, mask, onerror)]
  196. class autowatcher(watcher):
  197. '''watcher class that automatically watches newly created directories.'''
  198. __slots__ = (
  199. 'addfilter',
  200. )
  201. def __init__(self, addfilter=None):
  202. '''Create a new inotify instance.
  203. This instance will automatically watch newly created
  204. directories.
  205. If the optional addfilter parameter is not None, it must be a
  206. callable that takes one parameter. It will be called each time
  207. a directory is about to be automatically watched. If it returns
  208. True, the directory will be watched if it still exists,
  209. otherwise, it will beb skipped.'''
  210. super(autowatcher, self).__init__()
  211. self.addfilter = addfilter
  212. _dir_create_mask = inotify.IN_ISDIR | inotify.IN_CREATE
  213. def read(self, bufsize=None):
  214. events = super(autowatcher, self).read(bufsize)
  215. for evt in events:
  216. if evt.mask & self._dir_create_mask == self._dir_create_mask:
  217. if self.addfilter is None or self.addfilter(evt):
  218. parentmask = self._wds[evt.wd][1]
  219. # See note about race avoidance via IN_ONLYDIR above.
  220. mask = parentmask | inotify.IN_ONLYDIR
  221. try:
  222. self.add_all(evt.fullpath, mask)
  223. except OSError, err:
  224. if err.errno not in self.ignored_errors:
  225. raise
  226. return events
  227. class threshold(object):
  228. '''Class that indicates whether a file descriptor has reached a
  229. threshold of readable bytes available.
  230. This class is not thread-safe.'''
  231. __slots__ = (
  232. 'fd',
  233. 'threshold',
  234. '_iocbuf',
  235. )
  236. def __init__(self, fd, threshold=1024):
  237. self.fd = fd
  238. self.threshold = threshold
  239. self._iocbuf = array.array('i', [0])
  240. def readable(self):
  241. '''Return the number of bytes readable on this file descriptor.'''
  242. fcntl.ioctl(self.fd, termios.FIONREAD, self._iocbuf, True)
  243. return self._iocbuf[0]
  244. def __call__(self):
  245. '''Indicate whether the number of readable bytes has met or
  246. exceeded the threshold.'''
  247. return self.readable() >= self.threshold