1 # server.py - inotify status server
3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
9 from mercurial.i18n import _
10 from mercurial import osutil, util
12 import errno, os, select, socket, stat, struct, sys, tempfile, time
15 import linux as inotify
16 from linux import watcher
20 class AlreadyStartedException(Exception): pass
33 return path[:c], path[c+1:]
35 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
37 def walkrepodirs(dirstate, absroot):
38 '''Iterate over all subdirectories of this repo.
39 Exclude the .hg directory, any nested repos, and ignored dirs.'''
40 def walkit(dirname, top):
41 fullpath = join(absroot, dirname)
43 for name, kind in osutil.listdir(fullpath):
44 if kind == stat.S_IFDIR:
49 d = join(dirname, name)
50 if dirstate._ignore(d):
52 for subdir in walkit(d, False):
55 if err.errno not in walk_ignored_errors:
59 return walkit('', True)
61 def walk(dirstate, absroot, root):
62 '''Like os.walk, but only yields regular files.'''
64 # This function is critical to performance during startup.
66 def walkit(root, reporoot):
70 fullpath = join(absroot, root)
71 for name, kind in osutil.listdir(fullpath):
72 if kind == stat.S_IFDIR:
78 path = join(root, name)
79 if dirstate._ignore(path):
81 for result in walkit(path, False):
83 elif kind in (stat.S_IFREG, stat.S_IFLNK):
85 yield fullpath, dirs, files
88 if err.errno == errno.ENOTDIR:
89 # fullpath was a directory, but has since been replaced
91 yield fullpath, dirs, files
92 elif err.errno not in walk_ignored_errors:
95 return walkit(root, root == '')
97 def _explain_watch_limit(ui, dirstate, rootabs):
98 path = '/proc/sys/fs/inotify/max_user_watches'
100 limit = int(file(path).read())
102 if err.errno != errno.ENOENT:
104 raise util.Abort(_('this system does not seem to '
106 ui.warn(_('*** the current per-user limit on the number '
107 'of inotify watches is %s\n') % limit)
108 ui.warn(_('*** this limit is too low to watch every '
109 'directory in this repository\n'))
110 ui.warn(_('*** counting directories: '))
111 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
112 ui.warn(_('found %d\n') % ndirs)
113 newlimit = min(limit, 1024)
114 while newlimit < ((limit + ndirs) * 1.1):
116 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
118 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
119 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
122 class pollable(object):
124 Interface to support polling.
125 The file descriptor returned by fileno() is registered to a polling
128 Every tick, check if an event has happened since the last tick:
129 * If yes, call handle_events
130 * If no, call handle_timeout
132 poll_events = select.POLLIN
137 raise NotImplementedError
139 def handle_events(self, events):
140 raise NotImplementedError
142 def handle_timeout(self):
143 raise NotImplementedError
146 raise NotImplementedError
148 def register(self, timeout):
151 pollable.poll.register(fd, pollable.poll_events)
152 pollable.instances[fd] = self
154 self.registered = True
155 self.timeout = timeout
157 def unregister(self):
158 pollable.poll.unregister(self)
159 self.registered = False
166 for obj in cls.instances.itervalues():
167 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
168 timeout, timeobj = obj.timeout, obj
170 events = cls.poll.poll(timeout)
171 except select.error, err:
172 if err[0] == errno.EINTR:
177 for fd, event in events:
178 by_fd.setdefault(fd, []).append(event)
180 for fd, events in by_fd.iteritems():
181 cls.instances[fd].handle_pollevents(events)
184 timeobj.handle_timeout()
186 def eventaction(code):
188 Decorator to help handle events in repowatcher
191 def wrapper(self, wpath):
192 if code == 'm' and wpath in self.lastevent and \
193 self.lastevent[wpath] in 'cm':
195 self.lastevent[wpath] = code
200 wrapper.func_name = f.func_name
204 class directory(object):
206 Representing a directory
208 * path is the relative path from repo root to this directory
209 * files is a dict listing the files in this directory
210 - keys are file names
211 - values are file status
212 * dirs is a dict listing the subdirectories
213 - key are subdirectories names
214 - values are directory objects
216 def __init__(self, relpath=''):
221 def dir(self, relpath):
223 Returns the directory contained at the relative path relpath.
224 Creates the intermediate directories if necessary.
228 l = relpath.split('/')
235 d = directory(join(ret.path, next))
240 def walk(self, states):
242 yield (filename, status) pairs for items in the trees
243 that have status in states.
244 filenames are relative to the repo root
246 for file, st in self.files.iteritems():
248 yield join(self.path, file), st
249 for dir in self.dirs.itervalues():
250 for e in dir.walk(states):
253 def lookup(self, states, path):
255 yield root-relative filenames that match path, and whose
256 status are in states:
257 * if path is a file, yield path
258 * if path is a directory, yield directory files
259 * if path is not tracked, yield nothing
264 paths = path.split('/')
266 # we need to check separately for last node
272 tree = tree.dirs[dir]
274 # path is not tracked
278 # if path is a directory, walk it
279 for file, st in tree.dirs[last].walk(states):
283 if tree.files[last] in states:
287 # path is not tracked
290 class repowatcher(pollable):
292 Watches inotify events
294 statuskeys = 'almr!?'
299 inotify.IN_DELETE_SELF |
301 inotify.IN_MOVED_FROM |
302 inotify.IN_MOVED_TO |
303 inotify.IN_MOVE_SELF |
308 def __init__(self, ui, dirstate, root):
310 self.dirstate = dirstate
312 self.wprefix = join(root, '')
313 self.prefixlen = len(self.wprefix)
315 self.watcher = watcher.watcher()
317 raise util.Abort(_('inotify service not available: %s') %
319 self.threshold = watcher.threshold(self.watcher)
320 self.fileno = self.watcher.fileno
322 self.tree = directory()
324 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
326 self.last_event = None
330 self.register(timeout=None)
332 self.ds_info = self.dirstate_info()
333 self.handle_timeout()
336 def event_time(self):
337 last = self.last_event
339 self.last_event = now
345 return '+%.3f' % delta
347 return '+%.2f' % delta
348 return '+%.1f' % delta
350 def dirstate_info(self):
352 st = os.lstat(self.wprefix + '.hg/dirstate')
353 return st.st_mtime, st.st_ino
355 if err.errno != errno.ENOENT:
359 def add_watch(self, path, mask):
362 if self.watcher.path(path) is None:
363 if self.ui.debugflag:
364 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
366 self.watcher.add(path, mask)
368 if err.errno in (errno.ENOENT, errno.ENOTDIR):
370 if err.errno != errno.ENOSPC:
372 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
375 self.ui.note(_('watching directories under %r\n') % self.wprefix)
376 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
377 self.check_dirstate()
379 def filestatus(self, fn, st):
381 type_, mode, size, time = self.dirstate._map[fn][:4]
385 st_mode, st_size, st_mtime = st
388 if size and (size != st_size or (mode ^ st_mode) & 0100):
390 if time != int(st_mtime):
393 if type_ == '?' and self.dirstate._ignore(fn):
397 def updatefile(self, wfn, osstat):
399 update the file entry of an existing file.
401 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
404 self._updatestatus(wfn, self.filestatus(wfn, osstat))
406 def deletefile(self, wfn, oldstatus):
408 update the entry of a file which has been deleted.
410 oldstatus: char in statuskeys, status of the file before deletion
414 elif oldstatus in 'almn':
419 self.statcache.pop(wfn, None)
420 self._updatestatus(wfn, newstatus)
422 def _updatestatus(self, wfn, newstatus):
424 Update the stored status of a file.
426 newstatus: - char in (statuskeys + 'ni'), new status to apply.
427 - or None, to stop tracking wfn
429 root, fn = split(wfn)
430 d = self.tree.dir(root)
432 oldstatus = d.files.get(fn)
433 # oldstatus can be either:
435 # - a char in statuskeys: fn is a (tracked) file
437 if self.ui.debugflag and oldstatus != newstatus:
438 self.ui.note(_('status: %r %s -> %s\n') %
439 (wfn, oldstatus, newstatus))
441 if oldstatus and oldstatus in self.statuskeys \
442 and oldstatus != newstatus:
443 del self.statustrees[oldstatus].dir(root).files[fn]
445 if newstatus in (None, 'i'):
446 d.files.pop(fn, None)
447 elif oldstatus != newstatus:
448 d.files[fn] = newstatus
450 self.statustrees[newstatus].dir(root).files[fn] = newstatus
453 def check_deleted(self, key):
454 # Files that had been deleted but were present in the dirstate
455 # may have vanished from the dirstate; we must clean them up.
457 for wfn, ignore in self.statustrees[key].walk(key):
458 if wfn not in self.dirstate:
461 root, fn = split(wfn)
462 del self.statustrees[key].dir(root).files[fn]
463 del self.tree.dir(root).files[fn]
465 def scan(self, topdir=''):
466 ds = self.dirstate._map.copy()
467 self.add_watch(join(self.wprefix, topdir), self.mask)
468 for root, dirs, files in walk(self.dirstate, self.wprefix, topdir):
470 self.add_watch(join(root, d), self.mask)
471 wroot = root[self.prefixlen:]
473 wfn = join(wroot, fn)
474 self.updatefile(wfn, self.getstat(wfn))
477 if wtopdir and wtopdir[-1] != '/':
479 for wfn, state in ds.iteritems():
480 if not wfn.startswith(wtopdir):
486 self.deletefile(wfn, status)
488 self.updatefile(wfn, st)
489 self.check_deleted('!')
490 self.check_deleted('r')
492 def check_dirstate(self):
493 ds_info = self.dirstate_info()
494 if ds_info == self.ds_info:
496 self.ds_info = ds_info
497 if not self.ui.debugflag:
498 self.last_event = None
499 self.ui.note(_('%s dirstate reload\n') % self.event_time())
500 self.dirstate.invalidate()
501 self.handle_timeout()
503 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
505 def update_hgignore(self):
506 # An update of the ignore file can potentially change the
507 # states of all unknown and ignored files.
509 # XXX If the user has other ignore files outside the repo, or
510 # changes their list of ignore files at run time, we'll
511 # potentially never see changes to them. We could get the
512 # client to report to us what ignore data they're using.
513 # But it's easier to do nothing than to open that can of
516 if '_ignore' in self.dirstate.__dict__:
517 delattr(self.dirstate, '_ignore')
518 self.ui.note(_('rescanning due to .hgignore change\n'))
519 self.handle_timeout()
522 def getstat(self, wpath):
524 return self.statcache[wpath]
527 return self.stat(wpath)
529 if err.errno != errno.ENOENT:
532 def stat(self, wpath):
534 st = os.lstat(join(self.wprefix, wpath))
535 ret = st.st_mode, st.st_size, st.st_mtime
536 self.statcache[wpath] = ret
539 self.statcache.pop(wpath, None)
543 def created(self, wpath):
544 if wpath == '.hgignore':
545 self.update_hgignore()
547 st = self.stat(wpath)
548 if stat.S_ISREG(st[0]):
549 self.updatefile(wpath, st)
554 def modified(self, wpath):
555 if wpath == '.hgignore':
556 self.update_hgignore()
558 st = self.stat(wpath)
559 if stat.S_ISREG(st[0]):
560 if self.dirstate[wpath] in 'lmn':
561 self.updatefile(wpath, st)
566 def deleted(self, wpath):
567 if wpath == '.hgignore':
568 self.update_hgignore()
569 elif wpath.startswith('.hg/'):
570 if wpath == '.hg/wlock':
571 self.check_dirstate()
574 self.deletefile(wpath, self.dirstate[wpath])
576 def process_create(self, wpath, evt):
577 if self.ui.debugflag:
578 self.ui.note(_('%s event: created %s\n') %
579 (self.event_time(), wpath))
581 if evt.mask & inotify.IN_ISDIR:
586 def process_delete(self, wpath, evt):
587 if self.ui.debugflag:
588 self.ui.note(_('%s event: deleted %s\n') %
589 (self.event_time(), wpath))
591 if evt.mask & inotify.IN_ISDIR:
592 tree = self.tree.dir(wpath)
593 todelete = [wfn for wfn, ignore in tree.walk('?')]
595 self.deletefile(fn, '?')
600 def process_modify(self, wpath, evt):
601 if self.ui.debugflag:
602 self.ui.note(_('%s event: modified %s\n') %
603 (self.event_time(), wpath))
605 if not (evt.mask & inotify.IN_ISDIR):
608 def process_unmount(self, evt):
609 self.ui.warn(_('filesystem containing %s was unmounted\n') %
613 def handle_pollevents(self, events):
614 if self.ui.debugflag:
615 self.ui.note(_('%s readable: %d bytes\n') %
616 (self.event_time(), self.threshold.readable()))
617 if not self.threshold():
619 if self.ui.debugflag:
620 self.ui.note(_('%s below threshold - unhooking\n') %
627 def read_events(self, bufsize=None):
628 events = self.watcher.read(bufsize)
629 if self.ui.debugflag:
630 self.ui.note(_('%s reading %d events\n') %
631 (self.event_time(), len(events)))
633 assert evt.fullpath.startswith(self.wprefix)
634 wpath = evt.fullpath[self.prefixlen:]
636 # paths have been normalized, wpath never ends with a '/'
638 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
639 # ignore subdirectories of .hg/ (merge, patches...)
642 if evt.mask & inotify.IN_UNMOUNT:
643 self.process_unmount(wpath, evt)
644 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
645 self.process_modify(wpath, evt)
646 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
647 inotify.IN_MOVED_FROM):
648 self.process_delete(wpath, evt)
649 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
650 self.process_create(wpath, evt)
652 self.lastevent.clear()
654 def handle_timeout(self):
655 if not self.registered:
656 if self.ui.debugflag:
657 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
658 (self.event_time(), self.threshold.readable()))
660 self.register(timeout=None)
669 Returns a sorted list of relatives paths currently watched,
670 for debugging purposes.
672 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
674 class server(pollable):
676 Listens for client queries on unix socket inotify.sock
678 def __init__(self, ui, root, repowatcher, timeout):
680 self.repowatcher = repowatcher
681 self.sock = socket.socket(socket.AF_UNIX)
682 self.sockpath = join(root, '.hg/inotify.sock')
683 self.realsockpath = None
685 self.sock.bind(self.sockpath)
686 except socket.error, err:
687 if err[0] == errno.EADDRINUSE:
688 raise AlreadyStartedException(_('could not start server: %s')
690 if err[0] == "AF_UNIX path too long":
691 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
692 self.realsockpath = os.path.join(tempdir, "inotify.sock")
694 self.sock.bind(self.realsockpath)
695 os.symlink(self.realsockpath, self.sockpath)
696 except (OSError, socket.error), inst:
698 os.unlink(self.realsockpath)
702 if inst.errno == errno.EEXIST:
703 raise AlreadyStartedException(_('could not start server: %s')
709 self.fileno = self.sock.fileno
710 self.register(timeout=timeout)
712 def handle_timeout(self):
715 def answer_stat_query(self, cs):
716 names = cs.read().split('\0')
720 self.ui.note(_('answering query for %r\n') % states)
722 if self.repowatcher.timeout:
723 # We got a query while a rescan is pending. Make sure we
724 # rescan before responding, or we could give back a wrong
726 self.repowatcher.handle_timeout()
729 def genresult(states, tree):
730 for fn, state in tree.walk(states):
733 def genresult(states, tree):
735 for f in tree.lookup(states, fn):
738 return ['\0'.join(r) for r in [
739 genresult('l', self.repowatcher.statustrees['l']),
740 genresult('m', self.repowatcher.statustrees['m']),
741 genresult('a', self.repowatcher.statustrees['a']),
742 genresult('r', self.repowatcher.statustrees['r']),
743 genresult('!', self.repowatcher.statustrees['!']),
745 and genresult('?', self.repowatcher.statustrees['?'])
748 'c' in states and genresult('n', self.repowatcher.tree) or [],
751 def answer_dbug_query(self):
752 return ['\0'.join(self.repowatcher.debug())]
754 def handle_pollevents(self, events):
756 self.handle_pollevent()
758 def handle_pollevent(self):
759 sock, addr = self.sock.accept()
761 cs = common.recvcs(sock)
762 version = ord(cs.read(1))
764 if version != common.version:
765 self.ui.warn(_('received query from incompatible client '
766 'version %d\n') % version)
768 # try to send back our version to the client
769 # this way, the client too is informed of the mismatch
770 sock.sendall(chr(common.version))
778 results = self.answer_stat_query(cs)
780 results = self.answer_dbug_query()
782 self.ui.warn(_('unrecognized query type: %s\n') % type)
787 v = chr(common.version)
789 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
791 sock.sendall(''.join(results))
793 sock.shutdown(socket.SHUT_WR)
794 except socket.error, err:
795 if err[0] != errno.EPIPE:
801 os.unlink(self.sockpath)
802 if self.realsockpath:
803 os.unlink(self.realsockpath)
804 os.rmdir(os.path.dirname(self.realsockpath))
806 if err.errno != errno.ENOENT:
809 class master(object):
810 def __init__(self, ui, dirstate, root, timeout=None):
812 self.repowatcher = repowatcher(ui, dirstate, root)
813 self.server = server(ui, root, self.repowatcher, timeout)
816 for obj in pollable.instances.itervalues():
820 self.repowatcher.setup()
821 self.ui.note(_('finished setup\n'))
822 if os.getenv('TIME_STARTUP'):
826 def start(ui, dirstate, root):
827 def closefds(ignore):
828 # (from python bug #1177468)
829 # close all inherited file descriptors
830 # Python 2.4.1 and later use /dev/urandom to seed the random module's RNG
831 # a file descriptor is kept internally as os._urandomfd (created on demand
832 # the first time os.urandom() is called), and should not be closed
835 urandom_fd = getattr(os, '_urandomfd', None)
836 except AttributeError:
838 ignore.append(urandom_fd)
839 for fd in range(3, 256):
847 m = master(ui, dirstate, root)
855 closefds(pollable.instances.keys())
858 fd = os.open('/dev/null', os.O_RDONLY)
863 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
864 os.O_RDWR | os.O_CREAT | os.O_TRUNC)