summaryrefslogtreecommitdiff
path: root/hgext/inotify/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'hgext/inotify/server.py')
-rw-r--r--hgext/inotify/server.py492
1 files changed, 492 insertions, 0 deletions
diff --git a/hgext/inotify/server.py b/hgext/inotify/server.py
new file mode 100644
index 0000000..b654b17
--- /dev/null
+++ b/hgext/inotify/server.py
@@ -0,0 +1,492 @@
+# server.py - common entry point for inotify status server
+#
+# 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 or any later version.
+
+from mercurial.i18n import _
+from mercurial import cmdutil, osutil, util
+import common
+
+import errno
+import os
+import socket
+import stat
+import struct
+import sys
+import tempfile
+
+class AlreadyStartedException(Exception):
+ pass
+class TimeoutException(Exception):
+ pass
+
+def join(a, b):
+ if a:
+ if a[-1] == '/':
+ return a + b
+ return a + '/' + b
+ return b
+
+def split(path):
+ c = path.rfind('/')
+ if c == -1:
+ return '', path
+ return path[:c], path[c + 1:]
+
+walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
+
+def walk(dirstate, absroot, root):
+ '''Like os.walk, but only yields regular files.'''
+
+ # This function is critical to performance during startup.
+
+ def walkit(root, reporoot):
+ files, dirs = [], []
+
+ try:
+ fullpath = join(absroot, root)
+ for name, kind in osutil.listdir(fullpath):
+ if kind == stat.S_IFDIR:
+ if name == '.hg':
+ if not reporoot:
+ return
+ else:
+ dirs.append(name)
+ path = join(root, name)
+ if dirstate._ignore(path):
+ continue
+ for result in walkit(path, False):
+ yield result
+ elif kind in (stat.S_IFREG, stat.S_IFLNK):
+ files.append(name)
+ yield fullpath, dirs, files
+
+ except OSError, err:
+ if err.errno == errno.ENOTDIR:
+ # fullpath was a directory, but has since been replaced
+ # by a file.
+ yield fullpath, dirs, files
+ elif err.errno not in walk_ignored_errors:
+ raise
+
+ return walkit(root, root == '')
+
+class directory(object):
+ """
+ Representing a directory
+
+ * path is the relative path from repo root to this directory
+ * files is a dict listing the files in this directory
+ - keys are file names
+ - values are file status
+ * dirs is a dict listing the subdirectories
+ - key are subdirectories names
+ - values are directory objects
+ """
+ def __init__(self, relpath=''):
+ self.path = relpath
+ self.files = {}
+ self.dirs = {}
+
+ def dir(self, relpath):
+ """
+ Returns the directory contained at the relative path relpath.
+ Creates the intermediate directories if necessary.
+ """
+ if not relpath:
+ return self
+ l = relpath.split('/')
+ ret = self
+ while l:
+ next = l.pop(0)
+ try:
+ ret = ret.dirs[next]
+ except KeyError:
+ d = directory(join(ret.path, next))
+ ret.dirs[next] = d
+ ret = d
+ return ret
+
+ def walk(self, states, visited=None):
+ """
+ yield (filename, status) pairs for items in the trees
+ that have status in states.
+ filenames are relative to the repo root
+ """
+ for file, st in self.files.iteritems():
+ if st in states:
+ yield join(self.path, file), st
+ for dir in self.dirs.itervalues():
+ if visited is not None:
+ visited.add(dir.path)
+ for e in dir.walk(states):
+ yield e
+
+ def lookup(self, states, path, visited):
+ """
+ yield root-relative filenames that match path, and whose
+ status are in states:
+ * if path is a file, yield path
+ * if path is a directory, yield directory files
+ * if path is not tracked, yield nothing
+ """
+ if path[-1] == '/':
+ path = path[:-1]
+
+ paths = path.split('/')
+
+ # we need to check separately for last node
+ last = paths.pop()
+
+ tree = self
+ try:
+ for dir in paths:
+ tree = tree.dirs[dir]
+ except KeyError:
+ # path is not tracked
+ visited.add(tree.path)
+ return
+
+ try:
+ # if path is a directory, walk it
+ target = tree.dirs[last]
+ visited.add(target.path)
+ for file, st in target.walk(states, visited):
+ yield file
+ except KeyError:
+ try:
+ if tree.files[last] in states:
+ # path is a file
+ visited.add(tree.path)
+ yield path
+ except KeyError:
+ # path is not tracked
+ pass
+
+class repowatcher(object):
+ """
+ Watches inotify events
+ """
+ statuskeys = 'almr!?'
+
+ def __init__(self, ui, dirstate, root):
+ self.ui = ui
+ self.dirstate = dirstate
+
+ self.wprefix = join(root, '')
+ self.prefixlen = len(self.wprefix)
+
+ 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
+
+
+ def handle_timeout(self):
+ pass
+
+ def dirstate_info(self):
+ try:
+ st = os.lstat(self.wprefix + '.hg/dirstate')
+ return st.st_mtime, st.st_ino
+ except OSError, err:
+ if err.errno != errno.ENOENT:
+ raise
+ return 0, 0
+
+ def filestatus(self, fn, st):
+ try:
+ type_, mode, size, time = self.dirstate._map[fn][:4]
+ except KeyError:
+ type_ = '?'
+ if type_ == 'n':
+ st_mode, st_size, st_mtime = st
+ if size == -1:
+ return 'l'
+ if size and (size != st_size or (mode ^ st_mode) & 0100):
+ return 'm'
+ if time != int(st_mtime):
+ return 'l'
+ return 'n'
+ if type_ == '?' and self.dirstate._dirignore(fn):
+ # we must check not only if the file is ignored, but if any part
+ # of its path match an ignore pattern
+ return 'i'
+ return type_
+
+ def updatefile(self, wfn, osstat):
+ '''
+ update the file entry of an existing file.
+
+ osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
+ '''
+
+ self._updatestatus(wfn, self.filestatus(wfn, osstat))
+
+ def deletefile(self, wfn, oldstatus):
+ '''
+ update the entry of a file which has been deleted.
+
+ oldstatus: char in statuskeys, status of the file before deletion
+ '''
+ if oldstatus == 'r':
+ newstatus = 'r'
+ elif oldstatus in 'almn':
+ newstatus = '!'
+ else:
+ newstatus = None
+
+ self.statcache.pop(wfn, None)
+ self._updatestatus(wfn, newstatus)
+
+ def _updatestatus(self, wfn, newstatus):
+ '''
+ Update the stored status of a file.
+
+ newstatus: - char in (statuskeys + 'ni'), new status to apply.
+ - or None, to stop tracking wfn
+ '''
+ root, fn = split(wfn)
+ d = self.tree.dir(root)
+
+ oldstatus = d.files.get(fn)
+ # oldstatus can be either:
+ # - None : fn is new
+ # - a char in statuskeys: fn is a (tracked) file
+
+ if self.ui.debugflag and oldstatus != newstatus:
+ self.ui.note(_('status: %r %s -> %s\n') %
+ (wfn, oldstatus, newstatus))
+
+ if oldstatus and oldstatus in self.statuskeys \
+ and oldstatus != newstatus:
+ del self.statustrees[oldstatus].dir(root).files[fn]
+
+ if newstatus in (None, 'i'):
+ d.files.pop(fn, None)
+ elif oldstatus != newstatus:
+ d.files[fn] = newstatus
+ 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.
+ nuke = []
+ for wfn, ignore in self.statustrees[key].walk(key):
+ if wfn not in self.dirstate:
+ nuke.append(wfn)
+ for wfn in nuke:
+ root, fn = split(wfn)
+ del self.statustrees[key].dir(root).files[fn]
+ del self.tree.dir(root).files[fn]
+
+ def update_hgignore(self):
+ # An update of the ignore file can potentially change the
+ # states of all unknown and ignored files.
+
+ # XXX If the user has other ignore files outside the repo, or
+ # changes their list of ignore files at run time, we'll
+ # potentially never see changes to them. We could get the
+ # client to report to us what ignore data they're using.
+ # But it's easier to do nothing than to open that can of
+ # worms.
+
+ if '_ignore' in self.dirstate.__dict__:
+ delattr(self.dirstate, '_ignore')
+ self.ui.note(_('rescanning due to .hgignore change\n'))
+ self.handle_timeout()
+ self.scan()
+
+ def getstat(self, wpath):
+ try:
+ return self.statcache[wpath]
+ except KeyError:
+ try:
+ return self.stat(wpath)
+ except OSError, err:
+ if err.errno != errno.ENOENT:
+ raise
+
+ def stat(self, wpath):
+ try:
+ st = os.lstat(join(self.wprefix, wpath))
+ ret = st.st_mode, st.st_size, st.st_mtime
+ self.statcache[wpath] = ret
+ return ret
+ except OSError:
+ self.statcache.pop(wpath, None)
+ raise
+
+class socketlistener(object):
+ """
+ Listens for client queries on unix socket inotify.sock
+ """
+ def __init__(self, ui, root, repowatcher, timeout):
+ self.ui = ui
+ self.repowatcher = repowatcher
+ self.sock = socket.socket(socket.AF_UNIX)
+ self.sockpath = join(root, '.hg/inotify.sock')
+
+ self.realsockpath = self.sockpath
+ if os.path.islink(self.sockpath):
+ if os.path.exists(self.sockpath):
+ self.realsockpath = os.readlink(self.sockpath)
+ else:
+ raise util.Abort('inotify-server: cannot start: '
+ '.hg/inotify.sock is a broken symlink')
+ try:
+ self.sock.bind(self.realsockpath)
+ except socket.error, err:
+ if err.args[0] == errno.EADDRINUSE:
+ raise AlreadyStartedException(_('cannot start: socket is '
+ 'already bound'))
+ if err.args[0] == "AF_UNIX path too long":
+ tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
+ self.realsockpath = os.path.join(tempdir, "inotify.sock")
+ try:
+ self.sock.bind(self.realsockpath)
+ os.symlink(self.realsockpath, self.sockpath)
+ except (OSError, socket.error), inst:
+ try:
+ os.unlink(self.realsockpath)
+ except OSError:
+ pass
+ os.rmdir(tempdir)
+ if inst.errno == errno.EEXIST:
+ raise AlreadyStartedException(_('cannot start: tried '
+ 'linking .hg/inotify.sock to a temporary socket but'
+ ' .hg/inotify.sock already exists'))
+ raise
+ else:
+ raise
+ self.sock.listen(5)
+ self.fileno = self.sock.fileno
+
+ def answer_stat_query(self, cs):
+ names = cs.read().split('\0')
+
+ states = names.pop()
+
+ self.ui.note(_('answering query for %r\n') % states)
+
+ visited = set()
+ if not names:
+ def genresult(states, tree):
+ for fn, state in tree.walk(states):
+ yield fn
+ else:
+ def genresult(states, tree):
+ for fn in names:
+ for f in tree.lookup(states, fn, visited):
+ yield f
+
+ return ['\0'.join(r) for r in [
+ genresult('l', self.repowatcher.statustrees['l']),
+ genresult('m', self.repowatcher.statustrees['m']),
+ genresult('a', self.repowatcher.statustrees['a']),
+ genresult('r', self.repowatcher.statustrees['r']),
+ genresult('!', self.repowatcher.statustrees['!']),
+ '?' in states
+ and genresult('?', self.repowatcher.statustrees['?'])
+ or [],
+ [],
+ 'c' in states and genresult('n', self.repowatcher.tree) or [],
+ visited
+ ]]
+
+ def answer_dbug_query(self):
+ return ['\0'.join(self.repowatcher.debug())]
+
+ def accept_connection(self):
+ sock, addr = self.sock.accept()
+
+ cs = common.recvcs(sock)
+ version = ord(cs.read(1))
+
+ if version != common.version:
+ self.ui.warn(_('received query from incompatible client '
+ 'version %d\n') % version)
+ try:
+ # try to send back our version to the client
+ # this way, the client too is informed of the mismatch
+ sock.sendall(chr(common.version))
+ except socket.error:
+ pass
+ return
+
+ type = cs.read(4)
+
+ if type == 'STAT':
+ results = self.answer_stat_query(cs)
+ elif type == 'DBUG':
+ results = self.answer_dbug_query()
+ else:
+ self.ui.warn(_('unrecognized query type: %s\n') % type)
+ return
+
+ try:
+ try:
+ v = chr(common.version)
+
+ sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
+ *map(len, results)))
+ sock.sendall(''.join(results))
+ finally:
+ sock.shutdown(socket.SHUT_WR)
+ except socket.error, err:
+ if err.args[0] != errno.EPIPE:
+ raise
+
+if sys.platform.startswith('linux'):
+ import linuxserver as _server
+else:
+ raise ImportError
+
+master = _server.master
+
+def start(ui, dirstate, root, opts):
+ timeout = opts.get('idle_timeout')
+ if timeout:
+ timeout = float(timeout) * 60000
+ else:
+ timeout = None
+
+ class service(object):
+ def init(self):
+ try:
+ self.master = master(ui, dirstate, root, timeout)
+ except AlreadyStartedException, inst:
+ raise util.Abort("inotify-server: %s" % inst)
+
+ def run(self):
+ try:
+ try:
+ self.master.run()
+ except TimeoutException:
+ pass
+ finally:
+ self.master.shutdown()
+
+ if 'inserve' not in sys.argv:
+ runargs = util.hgcmd() + ['inserve', '-R', root]
+ else:
+ runargs = util.hgcmd() + sys.argv[1:]
+
+ pidfile = ui.config('inotify', 'pidfile')
+ if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
+ runargs.append("--pid-file=%s" % pidfile)
+
+ service = service()
+ logfile = ui.config('inotify', 'log')
+
+ appendpid = ui.configbool('inotify', 'appendpid', False)
+
+ ui.debug('starting inotify server: %s\n' % ' '.join(runargs))
+ cmdutil.service(opts, initfn=service.init, runfn=service.run,
+ logfile=logfile, runargs=runargs, appendpid=appendpid)