| # linuxserver.py - inotify status server for linux |
| # |
| # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com> |
| # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com> |
| # |
| # This software may be used and distributed according to the terms of the |
| # GNU General Public License version 2 or any later version. |
| |
| from mercurial.i18n import _ |
| from mercurial import osutil, util, error |
| import server |
| import errno, os, select, stat, sys, time |
| |
| try: |
| import linux as inotify |
| from linux import watcher |
| except ImportError: |
| raise |
| |
| def walkrepodirs(dirstate, absroot): |
| '''Iterate over all subdirectories of this repo. |
| Exclude the .hg directory, any nested repos, and ignored dirs.''' |
| def walkit(dirname, top): |
| fullpath = server.join(absroot, dirname) |
| try: |
| for name, kind in osutil.listdir(fullpath): |
| if kind == stat.S_IFDIR: |
| if name == '.hg': |
| if not top: |
| return |
| else: |
| d = server.join(dirname, name) |
| if dirstate._ignore(d): |
| continue |
| for subdir in walkit(d, False): |
| yield subdir |
| except OSError, err: |
| if err.errno not in server.walk_ignored_errors: |
| raise |
| yield fullpath |
| |
| return walkit('', True) |
| |
| def _explain_watch_limit(ui, dirstate, rootabs): |
| path = '/proc/sys/fs/inotify/max_user_watches' |
| try: |
| limit = int(util.readfile(path)) |
| except IOError, err: |
| if err.errno != errno.ENOENT: |
| raise |
| raise util.Abort(_('this system does not seem to ' |
| 'support inotify')) |
| ui.warn(_('*** the current per-user limit on the number ' |
| 'of inotify watches is %s\n') % limit) |
| ui.warn(_('*** this limit is too low to watch every ' |
| 'directory in this repository\n')) |
| ui.warn(_('*** counting directories: ')) |
| ndirs = len(list(walkrepodirs(dirstate, rootabs))) |
| ui.warn(_('found %d\n') % ndirs) |
| newlimit = min(limit, 1024) |
| while newlimit < ((limit + ndirs) * 1.1): |
| newlimit *= 2 |
| ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') % |
| (limit, newlimit)) |
| ui.warn(_('*** echo %d > %s\n') % (newlimit, path)) |
| raise util.Abort(_('cannot watch %s until inotify watch limit is raised') |
| % rootabs) |
| |
| class pollable(object): |
| """ |
| Interface to support polling. |
| The file descriptor returned by fileno() is registered to a polling |
| object. |
| Usage: |
| Every tick, check if an event has happened since the last tick: |
| * If yes, call handle_events |
| * If no, call handle_timeout |
| """ |
| poll_events = select.POLLIN |
| instances = {} |
| poll = select.poll() |
| |
| def fileno(self): |
| raise NotImplementedError |
| |
| def handle_events(self, events): |
| raise NotImplementedError |
| |
| def handle_timeout(self): |
| raise NotImplementedError |
| |
| def shutdown(self): |
| raise NotImplementedError |
| |
| def register(self, timeout): |
| fd = self.fileno() |
| |
| pollable.poll.register(fd, pollable.poll_events) |
| pollable.instances[fd] = self |
| |
| self.registered = True |
| self.timeout = timeout |
| |
| def unregister(self): |
| pollable.poll.unregister(self) |
| self.registered = False |
| |
| @classmethod |
| def run(cls): |
| while True: |
| timeout = None |
| timeobj = None |
| for obj in cls.instances.itervalues(): |
| if obj.timeout is not None and (timeout is None |
| or obj.timeout < timeout): |
| timeout, timeobj = obj.timeout, obj |
| try: |
| events = cls.poll.poll(timeout) |
| except select.error, err: |
| if err.args[0] == errno.EINTR: |
| continue |
| raise |
| if events: |
| by_fd = {} |
| for fd, event in events: |
| by_fd.setdefault(fd, []).append(event) |
| |
| for fd, events in by_fd.iteritems(): |
| cls.instances[fd].handle_pollevents(events) |
| |
| elif timeobj: |
| timeobj.handle_timeout() |
| |
| def eventaction(code): |
| """ |
| Decorator to help handle events in repowatcher |
| """ |
| def decorator(f): |
| def wrapper(self, wpath): |
| if code == 'm' and wpath in self.lastevent and \ |
| self.lastevent[wpath] in 'cm': |
| return |
| self.lastevent[wpath] = code |
| self.timeout = 250 |
| |
| f(self, wpath) |
| |
| wrapper.func_name = f.func_name |
| return wrapper |
| return decorator |
| |
| class repowatcher(server.repowatcher, pollable): |
| """ |
| Watches inotify events |
| """ |
| mask = ( |
| inotify.IN_ATTRIB | |
| inotify.IN_CREATE | |
| inotify.IN_DELETE | |
| inotify.IN_DELETE_SELF | |
| inotify.IN_MODIFY | |
| inotify.IN_MOVED_FROM | |
| inotify.IN_MOVED_TO | |
| inotify.IN_MOVE_SELF | |
| inotify.IN_ONLYDIR | |
| inotify.IN_UNMOUNT | |
| 0) |
| |
| def __init__(self, ui, dirstate, root): |
| server.repowatcher.__init__(self, ui, dirstate, root) |
| |
| self.lastevent = {} |
| self.dirty = False |
| try: |
| self.watcher = watcher.watcher() |
| except OSError, err: |
| raise util.Abort(_('inotify service not available: %s') % |
| err.strerror) |
| self.threshold = watcher.threshold(self.watcher) |
| self.fileno = self.watcher.fileno |
| self.register(timeout=None) |
| |
| self.handle_timeout() |
| self.scan() |
| |
| def event_time(self): |
| last = self.last_event |
| now = time.time() |
| self.last_event = now |
| |
| if last is None: |
| return 'start' |
| delta = now - last |
| if delta < 5: |
| return '+%.3f' % delta |
| if delta < 50: |
| return '+%.2f' % delta |
| return '+%.1f' % delta |
| |
| def add_watch(self, path, mask): |
| if not path: |
| return |
| if self.watcher.path(path) is None: |
| if self.ui.debugflag: |
| self.ui.note(_('watching %r\n') % path[self.prefixlen:]) |
| try: |
| self.watcher.add(path, mask) |
| except OSError, err: |
| if err.errno in (errno.ENOENT, errno.ENOTDIR): |
| return |
| if err.errno != errno.ENOSPC: |
| raise |
| _explain_watch_limit(self.ui, self.dirstate, self.wprefix) |
| |
| def setup(self): |
| self.ui.note(_('watching directories under %r\n') % self.wprefix) |
| self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE) |
| |
| def scan(self, topdir=''): |
| ds = self.dirstate._map.copy() |
| self.add_watch(server.join(self.wprefix, topdir), self.mask) |
| for root, dirs, files in server.walk(self.dirstate, self.wprefix, |
| topdir): |
| for d in dirs: |
| self.add_watch(server.join(root, d), self.mask) |
| wroot = root[self.prefixlen:] |
| for fn in files: |
| wfn = server.join(wroot, fn) |
| self.updatefile(wfn, self.getstat(wfn)) |
| ds.pop(wfn, None) |
| wtopdir = topdir |
| if wtopdir and wtopdir[-1] != '/': |
| wtopdir += '/' |
| for wfn, state in ds.iteritems(): |
| if not wfn.startswith(wtopdir): |
| continue |
| try: |
| st = self.stat(wfn) |
| except OSError: |
| status = state[0] |
| self.deletefile(wfn, status) |
| else: |
| self.updatefile(wfn, st) |
| self.check_deleted('!') |
| self.check_deleted('r') |
| |
| @eventaction('c') |
| def created(self, wpath): |
| if wpath == '.hgignore': |
| self.update_hgignore() |
| try: |
| st = self.stat(wpath) |
| if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]): |
| self.updatefile(wpath, st) |
| except OSError: |
| pass |
| |
| @eventaction('m') |
| def modified(self, wpath): |
| if wpath == '.hgignore': |
| self.update_hgignore() |
| try: |
| st = self.stat(wpath) |
| if stat.S_ISREG(st[0]): |
| if self.dirstate[wpath] in 'lmn': |
| self.updatefile(wpath, st) |
| except OSError: |
| pass |
| |
| @eventaction('d') |
| def deleted(self, wpath): |
| if wpath == '.hgignore': |
| self.update_hgignore() |
| elif wpath.startswith('.hg/'): |
| return |
| |
| self.deletefile(wpath, self.dirstate[wpath]) |
| |
| def process_create(self, wpath, evt): |
| if self.ui.debugflag: |
| self.ui.note(_('%s event: created %s\n') % |
| (self.event_time(), wpath)) |
| |
| if evt.mask & inotify.IN_ISDIR: |
| self.scan(wpath) |
| else: |
| self.created(wpath) |
| |
| def process_delete(self, wpath, evt): |
| if self.ui.debugflag: |
| self.ui.note(_('%s event: deleted %s\n') % |
| (self.event_time(), wpath)) |
| |
| if evt.mask & inotify.IN_ISDIR: |
| tree = self.tree.dir(wpath) |
| todelete = [wfn for wfn, ignore in tree.walk('?')] |
| for fn in todelete: |
| self.deletefile(fn, '?') |
| self.scan(wpath) |
| else: |
| self.deleted(wpath) |
| |
| def process_modify(self, wpath, evt): |
| if self.ui.debugflag: |
| self.ui.note(_('%s event: modified %s\n') % |
| (self.event_time(), wpath)) |
| |
| if not (evt.mask & inotify.IN_ISDIR): |
| self.modified(wpath) |
| |
| def process_unmount(self, evt): |
| self.ui.warn(_('filesystem containing %s was unmounted\n') % |
| evt.fullpath) |
| sys.exit(0) |
| |
| def handle_pollevents(self, events): |
| if self.ui.debugflag: |
| self.ui.note(_('%s readable: %d bytes\n') % |
| (self.event_time(), self.threshold.readable())) |
| if not self.threshold(): |
| if self.registered: |
| if self.ui.debugflag: |
| self.ui.note(_('%s below threshold - unhooking\n') % |
| (self.event_time())) |
| self.unregister() |
| self.timeout = 250 |
| else: |
| self.read_events() |
| |
| def read_events(self, bufsize=None): |
| events = self.watcher.read(bufsize) |
| if self.ui.debugflag: |
| self.ui.note(_('%s reading %d events\n') % |
| (self.event_time(), len(events))) |
| for evt in events: |
| if evt.fullpath == self.wprefix[:-1]: |
| # events on the root of the repository |
| # itself, e.g. permission changes or repository move |
| continue |
| assert evt.fullpath.startswith(self.wprefix) |
| wpath = evt.fullpath[self.prefixlen:] |
| |
| # paths have been normalized, wpath never ends with a '/' |
| |
| if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR: |
| # ignore subdirectories of .hg/ (merge, patches...) |
| continue |
| if wpath == ".hg/wlock": |
| if evt.mask & inotify.IN_DELETE: |
| self.dirstate.invalidate() |
| self.dirty = False |
| self.scan() |
| elif evt.mask & inotify.IN_CREATE: |
| self.dirty = True |
| else: |
| if self.dirty: |
| continue |
| |
| if evt.mask & inotify.IN_UNMOUNT: |
| self.process_unmount(wpath, evt) |
| elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB): |
| self.process_modify(wpath, evt) |
| elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF | |
| inotify.IN_MOVED_FROM): |
| self.process_delete(wpath, evt) |
| elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO): |
| self.process_create(wpath, evt) |
| |
| self.lastevent.clear() |
| |
| def handle_timeout(self): |
| if not self.registered: |
| if self.ui.debugflag: |
| self.ui.note(_('%s hooking back up with %d bytes readable\n') % |
| (self.event_time(), self.threshold.readable())) |
| self.read_events(0) |
| self.register(timeout=None) |
| |
| self.timeout = None |
| |
| def shutdown(self): |
| self.watcher.close() |
| |
| def debug(self): |
| """ |
| Returns a sorted list of relatives paths currently watched, |
| for debugging purposes. |
| """ |
| return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher) |
| |
| class socketlistener(server.socketlistener, pollable): |
| """ |
| Listens for client queries on unix socket inotify.sock |
| """ |
| def __init__(self, ui, root, repowatcher, timeout): |
| server.socketlistener.__init__(self, ui, root, repowatcher, timeout) |
| self.register(timeout=timeout) |
| |
| def handle_timeout(self): |
| raise server.TimeoutException |
| |
| def handle_pollevents(self, events): |
| for e in events: |
| self.accept_connection() |
| |
| def shutdown(self): |
| self.sock.close() |
| self.sock.cleanup() |
| |
| def answer_stat_query(self, cs): |
| if self.repowatcher.timeout: |
| # We got a query while a rescan is pending. Make sure we |
| # rescan before responding, or we could give back a wrong |
| # answer. |
| self.repowatcher.handle_timeout() |
| return server.socketlistener.answer_stat_query(self, cs) |
| |
| class master(object): |
| def __init__(self, ui, dirstate, root, timeout=None): |
| self.ui = ui |
| self.repowatcher = repowatcher(ui, dirstate, root) |
| self.socketlistener = socketlistener(ui, root, self.repowatcher, |
| timeout) |
| |
| def shutdown(self): |
| for obj in pollable.instances.itervalues(): |
| try: |
| obj.shutdown() |
| except error.SignalInterrupt: |
| pass |
| |
| def run(self): |
| self.repowatcher.setup() |
| self.ui.note(_('finished setup\n')) |
| if os.getenv('TIME_STARTUP'): |
| sys.exit(0) |
| pollable.run() |