PageRenderTime 49ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/hgext/inotify/linuxserver.py

https://bitbucket.org/mirror/mercurial/
Python | 441 lines | 428 code | 4 blank | 9 comment | 13 complexity | 1a13dc16c0eee14f15e117d12b786fac MD5 | raw file
Possible License(s): GPL-2.0
  1. # linuxserver.py - inotify status server for linux
  2. #
  3. # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
  4. # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
  5. #
  6. # This software may be used and distributed according to the terms of the
  7. # GNU General Public License version 2 or any later version.
  8. from mercurial.i18n import _
  9. from mercurial import osutil, util
  10. import server
  11. import errno, os, select, stat, sys, time
  12. try:
  13. import linux as inotify
  14. from linux import watcher
  15. except ImportError:
  16. raise
  17. def walkrepodirs(dirstate, absroot):
  18. '''Iterate over all subdirectories of this repo.
  19. Exclude the .hg directory, any nested repos, and ignored dirs.'''
  20. def walkit(dirname, top):
  21. fullpath = server.join(absroot, dirname)
  22. try:
  23. for name, kind in osutil.listdir(fullpath):
  24. if kind == stat.S_IFDIR:
  25. if name == '.hg':
  26. if not top:
  27. return
  28. else:
  29. d = server.join(dirname, name)
  30. if dirstate._ignore(d):
  31. continue
  32. for subdir in walkit(d, False):
  33. yield subdir
  34. except OSError, err:
  35. if err.errno not in server.walk_ignored_errors:
  36. raise
  37. yield fullpath
  38. return walkit('', True)
  39. def _explain_watch_limit(ui, dirstate, rootabs):
  40. path = '/proc/sys/fs/inotify/max_user_watches'
  41. try:
  42. limit = int(util.readfile(path))
  43. except IOError, err:
  44. if err.errno != errno.ENOENT:
  45. raise
  46. raise util.Abort(_('this system does not seem to '
  47. 'support inotify'))
  48. ui.warn(_('*** the current per-user limit on the number '
  49. 'of inotify watches is %s\n') % limit)
  50. ui.warn(_('*** this limit is too low to watch every '
  51. 'directory in this repository\n'))
  52. ui.warn(_('*** counting directories: '))
  53. ndirs = len(list(walkrepodirs(dirstate, rootabs)))
  54. ui.warn(_('found %d\n') % ndirs)
  55. newlimit = min(limit, 1024)
  56. while newlimit < ((limit + ndirs) * 1.1):
  57. newlimit *= 2
  58. ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
  59. (limit, newlimit))
  60. ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
  61. raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
  62. % rootabs)
  63. class pollable(object):
  64. """
  65. Interface to support polling.
  66. The file descriptor returned by fileno() is registered to a polling
  67. object.
  68. Usage:
  69. Every tick, check if an event has happened since the last tick:
  70. * If yes, call handle_events
  71. * If no, call handle_timeout
  72. """
  73. poll_events = select.POLLIN
  74. instances = {}
  75. poll = select.poll()
  76. def fileno(self):
  77. raise NotImplementedError
  78. def handle_events(self, events):
  79. raise NotImplementedError
  80. def handle_timeout(self):
  81. raise NotImplementedError
  82. def shutdown(self):
  83. raise NotImplementedError
  84. def register(self, timeout):
  85. fd = self.fileno()
  86. pollable.poll.register(fd, pollable.poll_events)
  87. pollable.instances[fd] = self
  88. self.registered = True
  89. self.timeout = timeout
  90. def unregister(self):
  91. pollable.poll.unregister(self)
  92. self.registered = False
  93. @classmethod
  94. def run(cls):
  95. while True:
  96. timeout = None
  97. timeobj = None
  98. for obj in cls.instances.itervalues():
  99. if obj.timeout is not None and (timeout is None
  100. or obj.timeout < timeout):
  101. timeout, timeobj = obj.timeout, obj
  102. try:
  103. events = cls.poll.poll(timeout)
  104. except select.error, err:
  105. if err.args[0] == errno.EINTR:
  106. continue
  107. raise
  108. if events:
  109. by_fd = {}
  110. for fd, event in events:
  111. by_fd.setdefault(fd, []).append(event)
  112. for fd, events in by_fd.iteritems():
  113. cls.instances[fd].handle_pollevents(events)
  114. elif timeobj:
  115. timeobj.handle_timeout()
  116. def eventaction(code):
  117. """
  118. Decorator to help handle events in repowatcher
  119. """
  120. def decorator(f):
  121. def wrapper(self, wpath):
  122. if code == 'm' and wpath in self.lastevent and \
  123. self.lastevent[wpath] in 'cm':
  124. return
  125. self.lastevent[wpath] = code
  126. self.timeout = 250
  127. f(self, wpath)
  128. wrapper.func_name = f.func_name
  129. return wrapper
  130. return decorator
  131. class repowatcher(server.repowatcher, pollable):
  132. """
  133. Watches inotify events
  134. """
  135. mask = (
  136. inotify.IN_ATTRIB |
  137. inotify.IN_CREATE |
  138. inotify.IN_DELETE |
  139. inotify.IN_DELETE_SELF |
  140. inotify.IN_MODIFY |
  141. inotify.IN_MOVED_FROM |
  142. inotify.IN_MOVED_TO |
  143. inotify.IN_MOVE_SELF |
  144. inotify.IN_ONLYDIR |
  145. inotify.IN_UNMOUNT |
  146. 0)
  147. def __init__(self, ui, dirstate, root):
  148. server.repowatcher.__init__(self, ui, dirstate, root)
  149. self.lastevent = {}
  150. self.dirty = False
  151. try:
  152. self.watcher = watcher.watcher()
  153. except OSError, err:
  154. raise util.Abort(_('inotify service not available: %s') %
  155. err.strerror)
  156. self.threshold = watcher.threshold(self.watcher)
  157. self.fileno = self.watcher.fileno
  158. self.register(timeout=None)
  159. self.handle_timeout()
  160. self.scan()
  161. def event_time(self):
  162. last = self.last_event
  163. now = time.time()
  164. self.last_event = now
  165. if last is None:
  166. return 'start'
  167. delta = now - last
  168. if delta < 5:
  169. return '+%.3f' % delta
  170. if delta < 50:
  171. return '+%.2f' % delta
  172. return '+%.1f' % delta
  173. def add_watch(self, path, mask):
  174. if not path:
  175. return
  176. if self.watcher.path(path) is None:
  177. if self.ui.debugflag:
  178. self.ui.note(_('watching %r\n') % path[self.prefixlen:])
  179. try:
  180. self.watcher.add(path, mask)
  181. except OSError, err:
  182. if err.errno in (errno.ENOENT, errno.ENOTDIR):
  183. return
  184. if err.errno != errno.ENOSPC:
  185. raise
  186. _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
  187. def setup(self):
  188. self.ui.note(_('watching directories under %r\n') % self.wprefix)
  189. self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
  190. def scan(self, topdir=''):
  191. ds = self.dirstate._map.copy()
  192. self.add_watch(server.join(self.wprefix, topdir), self.mask)
  193. for root, dirs, files in server.walk(self.dirstate, self.wprefix,
  194. topdir):
  195. for d in dirs:
  196. self.add_watch(server.join(root, d), self.mask)
  197. wroot = root[self.prefixlen:]
  198. for fn in files:
  199. wfn = server.join(wroot, fn)
  200. self.updatefile(wfn, self.getstat(wfn))
  201. ds.pop(wfn, None)
  202. wtopdir = topdir
  203. if wtopdir and wtopdir[-1] != '/':
  204. wtopdir += '/'
  205. for wfn, state in ds.iteritems():
  206. if not wfn.startswith(wtopdir):
  207. continue
  208. try:
  209. st = self.stat(wfn)
  210. except OSError:
  211. status = state[0]
  212. self.deletefile(wfn, status)
  213. else:
  214. self.updatefile(wfn, st)
  215. self.check_deleted('!')
  216. self.check_deleted('r')
  217. @eventaction('c')
  218. def created(self, wpath):
  219. if wpath == '.hgignore':
  220. self.update_hgignore()
  221. try:
  222. st = self.stat(wpath)
  223. if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
  224. self.updatefile(wpath, st)
  225. except OSError:
  226. pass
  227. @eventaction('m')
  228. def modified(self, wpath):
  229. if wpath == '.hgignore':
  230. self.update_hgignore()
  231. try:
  232. st = self.stat(wpath)
  233. if stat.S_ISREG(st[0]):
  234. if self.dirstate[wpath] in 'lmn':
  235. self.updatefile(wpath, st)
  236. except OSError:
  237. pass
  238. @eventaction('d')
  239. def deleted(self, wpath):
  240. if wpath == '.hgignore':
  241. self.update_hgignore()
  242. elif wpath.startswith('.hg/'):
  243. return
  244. self.deletefile(wpath, self.dirstate[wpath])
  245. def process_create(self, wpath, evt):
  246. if self.ui.debugflag:
  247. self.ui.note(_('%s event: created %s\n') %
  248. (self.event_time(), wpath))
  249. if evt.mask & inotify.IN_ISDIR:
  250. self.scan(wpath)
  251. else:
  252. self.created(wpath)
  253. def process_delete(self, wpath, evt):
  254. if self.ui.debugflag:
  255. self.ui.note(_('%s event: deleted %s\n') %
  256. (self.event_time(), wpath))
  257. if evt.mask & inotify.IN_ISDIR:
  258. tree = self.tree.dir(wpath)
  259. todelete = [wfn for wfn, ignore in tree.walk('?')]
  260. for fn in todelete:
  261. self.deletefile(fn, '?')
  262. self.scan(wpath)
  263. else:
  264. self.deleted(wpath)
  265. def process_modify(self, wpath, evt):
  266. if self.ui.debugflag:
  267. self.ui.note(_('%s event: modified %s\n') %
  268. (self.event_time(), wpath))
  269. if not (evt.mask & inotify.IN_ISDIR):
  270. self.modified(wpath)
  271. def process_unmount(self, evt):
  272. self.ui.warn(_('filesystem containing %s was unmounted\n') %
  273. evt.fullpath)
  274. sys.exit(0)
  275. def handle_pollevents(self, events):
  276. if self.ui.debugflag:
  277. self.ui.note(_('%s readable: %d bytes\n') %
  278. (self.event_time(), self.threshold.readable()))
  279. if not self.threshold():
  280. if self.registered:
  281. if self.ui.debugflag:
  282. self.ui.note(_('%s below threshold - unhooking\n') %
  283. (self.event_time()))
  284. self.unregister()
  285. self.timeout = 250
  286. else:
  287. self.read_events()
  288. def read_events(self, bufsize=None):
  289. events = self.watcher.read(bufsize)
  290. if self.ui.debugflag:
  291. self.ui.note(_('%s reading %d events\n') %
  292. (self.event_time(), len(events)))
  293. for evt in events:
  294. if evt.fullpath == self.wprefix[:-1]:
  295. # events on the root of the repository
  296. # itself, e.g. permission changes or repository move
  297. continue
  298. assert evt.fullpath.startswith(self.wprefix)
  299. wpath = evt.fullpath[self.prefixlen:]
  300. # paths have been normalized, wpath never ends with a '/'
  301. if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
  302. # ignore subdirectories of .hg/ (merge, patches...)
  303. continue
  304. if wpath == ".hg/wlock":
  305. if evt.mask & inotify.IN_DELETE:
  306. self.dirstate.invalidate()
  307. self.dirty = False
  308. self.scan()
  309. elif evt.mask & inotify.IN_CREATE:
  310. self.dirty = True
  311. else:
  312. if self.dirty:
  313. continue
  314. if evt.mask & inotify.IN_UNMOUNT:
  315. self.process_unmount(wpath, evt)
  316. elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
  317. self.process_modify(wpath, evt)
  318. elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
  319. inotify.IN_MOVED_FROM):
  320. self.process_delete(wpath, evt)
  321. elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
  322. self.process_create(wpath, evt)
  323. self.lastevent.clear()
  324. def handle_timeout(self):
  325. if not self.registered:
  326. if self.ui.debugflag:
  327. self.ui.note(_('%s hooking back up with %d bytes readable\n') %
  328. (self.event_time(), self.threshold.readable()))
  329. self.read_events(0)
  330. self.register(timeout=None)
  331. self.timeout = None
  332. def shutdown(self):
  333. self.watcher.close()
  334. def debug(self):
  335. """
  336. Returns a sorted list of relatives paths currently watched,
  337. for debugging purposes.
  338. """
  339. return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
  340. class socketlistener(server.socketlistener, pollable):
  341. """
  342. Listens for client queries on unix socket inotify.sock
  343. """
  344. def __init__(self, ui, root, repowatcher, timeout):
  345. server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
  346. self.register(timeout=timeout)
  347. def handle_timeout(self):
  348. raise server.TimeoutException
  349. def handle_pollevents(self, events):
  350. for e in events:
  351. self.accept_connection()
  352. def shutdown(self):
  353. self.sock.close()
  354. try:
  355. os.unlink(self.sockpath)
  356. if self.realsockpath:
  357. os.unlink(self.realsockpath)
  358. os.rmdir(os.path.dirname(self.realsockpath))
  359. except OSError, err:
  360. if err.errno != errno.ENOENT:
  361. raise
  362. def answer_stat_query(self, cs):
  363. if self.repowatcher.timeout:
  364. # We got a query while a rescan is pending. Make sure we
  365. # rescan before responding, or we could give back a wrong
  366. # answer.
  367. self.repowatcher.handle_timeout()
  368. return server.socketlistener.answer_stat_query(self, cs)
  369. class master(object):
  370. def __init__(self, ui, dirstate, root, timeout=None):
  371. self.ui = ui
  372. self.repowatcher = repowatcher(ui, dirstate, root)
  373. self.socketlistener = socketlistener(ui, root, self.repowatcher,
  374. timeout)
  375. def shutdown(self):
  376. for obj in pollable.instances.itervalues():
  377. obj.shutdown()
  378. def run(self):
  379. self.repowatcher.setup()
  380. self.ui.note(_('finished setup\n'))
  381. if os.getenv('TIME_STARTUP'):
  382. sys.exit(0)
  383. pollable.run()