--- a/hgext/inotify/server.py Wed Nov 25 00:02:02 2009 +0200
+++ b/hgext/inotify/server.py Fri Aug 14 08:19:49 2009 -0400
@@ -1,7 +1,6 @@
-# server.py - inotify status server
+# server.py - common entry point for inotify status server
#
-# Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
-# Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
+# Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.
@@ -9,13 +8,14 @@
from mercurial.i18n import _
from mercurial import cmdutil, osutil, util
import common
-import errno, os, select, socket, stat, struct, sys, tempfile, time
-try:
- import linux as inotify
- from linux import watcher
-except ImportError:
- raise
+import errno
+import os
+import socket
+import stat
+import struct
+import sys
+import tempfile
class AlreadyStartedException(Exception): pass
@@ -34,30 +34,6 @@
walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
-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 = 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 = join(dirname, name)
- if dirstate._ignore(d):
- continue
- for subdir in walkit(d, False):
- yield subdir
- except OSError, err:
- if err.errno not in walk_ignored_errors:
- raise
- yield fullpath
-
- return walkit('', True)
-
def walk(dirstate, absroot, root):
'''Like os.walk, but only yields regular files.'''
@@ -94,113 +70,6 @@
return walkit(root, root == '')
-def _explain_watch_limit(ui, dirstate, rootabs):
- path = '/proc/sys/fs/inotify/max_user_watches'
- try:
- limit = int(file(path).read())
- 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[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 directory(object):
"""
Representing a directory
@@ -293,23 +162,11 @@
# path is not tracked
pass
-class repowatcher(pollable):
+class repowatcher(object):
"""
Watches inotify events
"""
statuskeys = 'almr!?'
- 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):
self.ui = ui
@@ -317,41 +174,18 @@
self.wprefix = join(root, '')
self.prefixlen = len(self.wprefix)
- 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.tree = directory()
self.statcache = {}
self.statustrees = dict([(s, directory()) for s in self.statuskeys])
+ self.ds_info = self.dirstate_info()
+
self.last_event = None
- self.lastevent = {}
- self.register(timeout=None)
-
- self.ds_info = self.dirstate_info()
- 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 handle_timeout(self):
+ pass
def dirstate_info(self):
try:
@@ -362,26 +196,6 @@
raise
return 0, 0
- 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)
- self.check_dirstate()
-
def filestatus(self, fn, st):
try:
type_, mode, size, time = self.dirstate._map[fn][:4]
@@ -455,7 +269,6 @@
if newstatus != 'n':
self.statustrees[newstatus].dir(root).files[fn] = newstatus
-
def check_deleted(self, key):
# Files that had been deleted but were present in the dirstate
# may have vanished from the dirstate; we must clean them up.
@@ -468,33 +281,6 @@
del self.statustrees[key].dir(root).files[fn]
del self.tree.dir(root).files[fn]
- def scan(self, topdir=''):
- ds = self.dirstate._map.copy()
- self.add_watch(join(self.wprefix, topdir), self.mask)
- for root, dirs, files in walk(self.dirstate, self.wprefix, topdir):
- for d in dirs:
- self.add_watch(join(root, d), self.mask)
- wroot = root[self.prefixlen:]
- for fn in files:
- wfn = 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')
-
def check_dirstate(self):
ds_info = self.dirstate_info()
if ds_info == self.ds_info:
@@ -502,11 +288,9 @@
self.ds_info = ds_info
if not self.ui.debugflag:
self.last_event = None
- self.ui.note(_('%s dirstate reload\n') % self.event_time())
self.dirstate.invalidate()
self.handle_timeout()
self.scan()
- self.ui.note(_('%s end dirstate reload\n') % self.event_time())
def update_hgignore(self):
# An update of the ignore file can potentially change the
@@ -545,139 +329,7 @@
self.statcache.pop(wpath, None)
raise
- @eventaction('c')
- def created(self, wpath):
- if wpath == '.hgignore':
- self.update_hgignore()
- try:
- st = self.stat(wpath)
- if stat.S_ISREG(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/'):
- if wpath == '.hg/wlock':
- self.check_dirstate()
- 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:
- 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 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 server(pollable):
+class socketlistener(object):
"""
Listens for client queries on unix socket inotify.sock
"""
@@ -718,10 +370,6 @@
raise
self.sock.listen(5)
self.fileno = self.sock.fileno
- self.register(timeout=timeout)
-
- def handle_timeout(self):
- pass
def answer_stat_query(self, cs):
names = cs.read().split('\0')
@@ -730,12 +378,6 @@
self.ui.note(_('answering query for %r\n') % states)
- 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()
-
visited = set()
if not names:
def genresult(states, tree):
@@ -764,11 +406,7 @@
def answer_dbug_query(self):
return ['\0'.join(self.repowatcher.debug())]
- def handle_pollevents(self, events):
- for e in events:
- self.handle_pollevent()
-
- def handle_pollevent(self):
+ def accept_connection(self):
sock, addr = self.sock.accept()
cs = common.recvcs(sock)
@@ -808,33 +446,12 @@
if err[0] != errno.EPIPE:
raise
- def shutdown(self):
- self.sock.close()
- try:
- os.unlink(self.sockpath)
- if self.realsockpath:
- os.unlink(self.realsockpath)
- os.rmdir(os.path.dirname(self.realsockpath))
- except OSError, err:
- if err.errno != errno.ENOENT:
- raise
+if sys.platform == 'linux2':
+ import linuxserver as _server
+else:
+ raise ImportError
-class master(object):
- def __init__(self, ui, dirstate, root, timeout=None):
- self.ui = ui
- self.repowatcher = repowatcher(ui, dirstate, root)
- self.server = server(ui, root, self.repowatcher, timeout)
-
- def shutdown(self):
- for obj in pollable.instances.itervalues():
- obj.shutdown()
-
- def run(self):
- self.repowatcher.setup()
- self.ui.note(_('finished setup\n'))
- if os.getenv('TIME_STARTUP'):
- sys.exit(0)
- pollable.run()
+master = _server.master
def start(ui, dirstate, root, opts):
timeout = opts.get('timeout')