hgext/inotify/server.py
branchstable
changeset 21160 564f55b25122
parent 21028 a0f437e2f5a9
parent 21159 024f38f6d5f6
child 21161 ef59019f4771
equal deleted inserted replaced
21028:a0f437e2f5a9 21160:564f55b25122
     1 # server.py - common entry point for inotify status server
       
     2 #
       
     3 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 from mercurial.i18n import _
       
     9 from mercurial import cmdutil, posix, osutil, util
       
    10 import common
       
    11 
       
    12 import errno
       
    13 import os
       
    14 import socket
       
    15 import stat
       
    16 import struct
       
    17 import sys
       
    18 
       
    19 class AlreadyStartedException(Exception):
       
    20     pass
       
    21 class TimeoutException(Exception):
       
    22     pass
       
    23 
       
    24 def join(a, b):
       
    25     if a:
       
    26         if a[-1] == '/':
       
    27             return a + b
       
    28         return a + '/' + b
       
    29     return b
       
    30 
       
    31 def split(path):
       
    32     c = path.rfind('/')
       
    33     if c == -1:
       
    34         return '', path
       
    35     return path[:c], path[c + 1:]
       
    36 
       
    37 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
       
    38 
       
    39 def walk(dirstate, absroot, root):
       
    40     '''Like os.walk, but only yields regular files.'''
       
    41 
       
    42     # This function is critical to performance during startup.
       
    43 
       
    44     def walkit(root, reporoot):
       
    45         files, dirs = [], []
       
    46 
       
    47         try:
       
    48             fullpath = join(absroot, root)
       
    49             for name, kind in osutil.listdir(fullpath):
       
    50                 if kind == stat.S_IFDIR:
       
    51                     if name == '.hg':
       
    52                         if not reporoot:
       
    53                             return
       
    54                     else:
       
    55                         dirs.append(name)
       
    56                         path = join(root, name)
       
    57                         if dirstate._ignore(path):
       
    58                             continue
       
    59                         for result in walkit(path, False):
       
    60                             yield result
       
    61                 elif kind in (stat.S_IFREG, stat.S_IFLNK):
       
    62                     files.append(name)
       
    63             yield fullpath, dirs, files
       
    64 
       
    65         except OSError, err:
       
    66             if err.errno == errno.ENOTDIR:
       
    67                 # fullpath was a directory, but has since been replaced
       
    68                 # by a file.
       
    69                 yield fullpath, dirs, files
       
    70             elif err.errno not in walk_ignored_errors:
       
    71                 raise
       
    72 
       
    73     return walkit(root, root == '')
       
    74 
       
    75 class directory(object):
       
    76     """
       
    77     Representing a directory
       
    78 
       
    79     * path is the relative path from repo root to this directory
       
    80     * files is a dict listing the files in this directory
       
    81         - keys are file names
       
    82         - values are file status
       
    83     * dirs is a dict listing the subdirectories
       
    84         - key are subdirectories names
       
    85         - values are directory objects
       
    86     """
       
    87     def __init__(self, relpath=''):
       
    88         self.path = relpath
       
    89         self.files = {}
       
    90         self.dirs = {}
       
    91 
       
    92     def dir(self, relpath):
       
    93         """
       
    94         Returns the directory contained at the relative path relpath.
       
    95         Creates the intermediate directories if necessary.
       
    96         """
       
    97         if not relpath:
       
    98             return self
       
    99         l = relpath.split('/')
       
   100         ret = self
       
   101         while l:
       
   102             next = l.pop(0)
       
   103             try:
       
   104                 ret = ret.dirs[next]
       
   105             except KeyError:
       
   106                 d = directory(join(ret.path, next))
       
   107                 ret.dirs[next] = d
       
   108                 ret = d
       
   109         return ret
       
   110 
       
   111     def walk(self, states, visited=None):
       
   112         """
       
   113         yield (filename, status) pairs for items in the trees
       
   114         that have status in states.
       
   115         filenames are relative to the repo root
       
   116         """
       
   117         for file, st in self.files.iteritems():
       
   118             if st in states:
       
   119                 yield join(self.path, file), st
       
   120         for dir in self.dirs.itervalues():
       
   121             if visited is not None:
       
   122                 visited.add(dir.path)
       
   123             for e in dir.walk(states):
       
   124                 yield e
       
   125 
       
   126     def lookup(self, states, path, visited):
       
   127         """
       
   128         yield root-relative filenames that match path, and whose
       
   129         status are in states:
       
   130         * if path is a file, yield path
       
   131         * if path is a directory, yield directory files
       
   132         * if path is not tracked, yield nothing
       
   133         """
       
   134         if path[-1] == '/':
       
   135             path = path[:-1]
       
   136 
       
   137         paths = path.split('/')
       
   138 
       
   139         # we need to check separately for last node
       
   140         last = paths.pop()
       
   141 
       
   142         tree = self
       
   143         try:
       
   144             for dir in paths:
       
   145                 tree = tree.dirs[dir]
       
   146         except KeyError:
       
   147             # path is not tracked
       
   148             visited.add(tree.path)
       
   149             return
       
   150 
       
   151         try:
       
   152             # if path is a directory, walk it
       
   153             target = tree.dirs[last]
       
   154             visited.add(target.path)
       
   155             for file, st in target.walk(states, visited):
       
   156                 yield file
       
   157         except KeyError:
       
   158             try:
       
   159                 if tree.files[last] in states:
       
   160                     # path is a file
       
   161                     visited.add(tree.path)
       
   162                     yield path
       
   163             except KeyError:
       
   164                 # path is not tracked
       
   165                 pass
       
   166 
       
   167 class repowatcher(object):
       
   168     """
       
   169     Watches inotify events
       
   170     """
       
   171     statuskeys = 'almr!?'
       
   172 
       
   173     def __init__(self, ui, dirstate, root):
       
   174         self.ui = ui
       
   175         self.dirstate = dirstate
       
   176 
       
   177         self.wprefix = join(root, '')
       
   178         self.prefixlen = len(self.wprefix)
       
   179 
       
   180         self.tree = directory()
       
   181         self.statcache = {}
       
   182         self.statustrees = dict([(s, directory()) for s in self.statuskeys])
       
   183 
       
   184         self.ds_info = self.dirstate_info()
       
   185 
       
   186         self.last_event = None
       
   187 
       
   188 
       
   189     def handle_timeout(self):
       
   190         pass
       
   191 
       
   192     def dirstate_info(self):
       
   193         try:
       
   194             st = os.lstat(self.wprefix + '.hg/dirstate')
       
   195             return st.st_mtime, st.st_ino
       
   196         except OSError, err:
       
   197             if err.errno != errno.ENOENT:
       
   198                 raise
       
   199             return 0, 0
       
   200 
       
   201     def filestatus(self, fn, st):
       
   202         try:
       
   203             type_, mode, size, time = self.dirstate._map[fn][:4]
       
   204         except KeyError:
       
   205             type_ = '?'
       
   206         if type_ == 'n':
       
   207             st_mode, st_size, st_mtime = st
       
   208             if size == -1:
       
   209                 return 'l'
       
   210             if size and (size != st_size or (mode ^ st_mode) & 0100):
       
   211                 return 'm'
       
   212             if time != int(st_mtime):
       
   213                 return 'l'
       
   214             return 'n'
       
   215         if type_ == '?' and self.dirstate._dirignore(fn):
       
   216             # we must check not only if the file is ignored, but if any part
       
   217             # of its path match an ignore pattern
       
   218             return 'i'
       
   219         return type_
       
   220 
       
   221     def updatefile(self, wfn, osstat):
       
   222         '''
       
   223         update the file entry of an existing file.
       
   224 
       
   225         osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
       
   226         '''
       
   227 
       
   228         self._updatestatus(wfn, self.filestatus(wfn, osstat))
       
   229 
       
   230     def deletefile(self, wfn, oldstatus):
       
   231         '''
       
   232         update the entry of a file which has been deleted.
       
   233 
       
   234         oldstatus: char in statuskeys, status of the file before deletion
       
   235         '''
       
   236         if oldstatus == 'r':
       
   237             newstatus = 'r'
       
   238         elif oldstatus in 'almn':
       
   239             newstatus = '!'
       
   240         else:
       
   241             newstatus = None
       
   242 
       
   243         self.statcache.pop(wfn, None)
       
   244         self._updatestatus(wfn, newstatus)
       
   245 
       
   246     def _updatestatus(self, wfn, newstatus):
       
   247         '''
       
   248         Update the stored status of a file.
       
   249 
       
   250         newstatus: - char in (statuskeys + 'ni'), new status to apply.
       
   251                    - or None, to stop tracking wfn
       
   252         '''
       
   253         root, fn = split(wfn)
       
   254         d = self.tree.dir(root)
       
   255 
       
   256         oldstatus = d.files.get(fn)
       
   257         # oldstatus can be either:
       
   258         # - None : fn is new
       
   259         # - a char in statuskeys: fn is a (tracked) file
       
   260 
       
   261         if self.ui.debugflag and oldstatus != newstatus:
       
   262             self.ui.note(_('status: %r %s -> %s\n') %
       
   263                              (wfn, oldstatus, newstatus))
       
   264 
       
   265         if oldstatus and oldstatus in self.statuskeys \
       
   266             and oldstatus != newstatus:
       
   267             del self.statustrees[oldstatus].dir(root).files[fn]
       
   268 
       
   269         if newstatus in (None, 'i'):
       
   270             d.files.pop(fn, None)
       
   271         elif oldstatus != newstatus:
       
   272             d.files[fn] = newstatus
       
   273             if newstatus != 'n':
       
   274                 self.statustrees[newstatus].dir(root).files[fn] = newstatus
       
   275 
       
   276     def check_deleted(self, key):
       
   277         # Files that had been deleted but were present in the dirstate
       
   278         # may have vanished from the dirstate; we must clean them up.
       
   279         nuke = []
       
   280         for wfn, ignore in self.statustrees[key].walk(key):
       
   281             if wfn not in self.dirstate:
       
   282                 nuke.append(wfn)
       
   283         for wfn in nuke:
       
   284             root, fn = split(wfn)
       
   285             del self.statustrees[key].dir(root).files[fn]
       
   286             del self.tree.dir(root).files[fn]
       
   287 
       
   288     def update_hgignore(self):
       
   289         # An update of the ignore file can potentially change the
       
   290         # states of all unknown and ignored files.
       
   291 
       
   292         # XXX If the user has other ignore files outside the repo, or
       
   293         # changes their list of ignore files at run time, we'll
       
   294         # potentially never see changes to them.  We could get the
       
   295         # client to report to us what ignore data they're using.
       
   296         # But it's easier to do nothing than to open that can of
       
   297         # worms.
       
   298 
       
   299         if '_ignore' in self.dirstate.__dict__:
       
   300             delattr(self.dirstate, '_ignore')
       
   301             self.ui.note(_('rescanning due to .hgignore change\n'))
       
   302             self.handle_timeout()
       
   303             self.scan()
       
   304 
       
   305     def getstat(self, wpath):
       
   306         try:
       
   307             return self.statcache[wpath]
       
   308         except KeyError:
       
   309             try:
       
   310                 return self.stat(wpath)
       
   311             except OSError, err:
       
   312                 if err.errno != errno.ENOENT:
       
   313                     raise
       
   314 
       
   315     def stat(self, wpath):
       
   316         try:
       
   317             st = os.lstat(join(self.wprefix, wpath))
       
   318             ret = st.st_mode, st.st_size, st.st_mtime
       
   319             self.statcache[wpath] = ret
       
   320             return ret
       
   321         except OSError:
       
   322             self.statcache.pop(wpath, None)
       
   323             raise
       
   324 
       
   325 class socketlistener(object):
       
   326     """
       
   327     Listens for client queries on unix socket inotify.sock
       
   328     """
       
   329     def __init__(self, ui, root, repowatcher, timeout):
       
   330         self.ui = ui
       
   331         self.repowatcher = repowatcher
       
   332         try:
       
   333             self.sock = posix.unixdomainserver(
       
   334                 lambda p: os.path.join(root, '.hg', p),
       
   335                 'inotify')
       
   336         except (OSError, socket.error), err:
       
   337             if err.args[0] == errno.EADDRINUSE:
       
   338                 raise AlreadyStartedException(_('cannot start: '
       
   339                                                 'socket is already bound'))
       
   340             raise
       
   341         self.fileno = self.sock.fileno
       
   342 
       
   343     def answer_stat_query(self, cs):
       
   344         names = cs.read().split('\0')
       
   345 
       
   346         states = names.pop()
       
   347 
       
   348         self.ui.note(_('answering query for %r\n') % states)
       
   349 
       
   350         visited = set()
       
   351         if not names:
       
   352             def genresult(states, tree):
       
   353                 for fn, state in tree.walk(states):
       
   354                     yield fn
       
   355         else:
       
   356             def genresult(states, tree):
       
   357                 for fn in names:
       
   358                     for f in tree.lookup(states, fn, visited):
       
   359                         yield f
       
   360 
       
   361         return ['\0'.join(r) for r in [
       
   362             genresult('l', self.repowatcher.statustrees['l']),
       
   363             genresult('m', self.repowatcher.statustrees['m']),
       
   364             genresult('a', self.repowatcher.statustrees['a']),
       
   365             genresult('r', self.repowatcher.statustrees['r']),
       
   366             genresult('!', self.repowatcher.statustrees['!']),
       
   367             '?' in states
       
   368                 and genresult('?', self.repowatcher.statustrees['?'])
       
   369                 or [],
       
   370             [],
       
   371             'c' in states and genresult('n', self.repowatcher.tree) or [],
       
   372             visited
       
   373             ]]
       
   374 
       
   375     def answer_dbug_query(self):
       
   376         return ['\0'.join(self.repowatcher.debug())]
       
   377 
       
   378     def accept_connection(self):
       
   379         sock, addr = self.sock.accept()
       
   380 
       
   381         cs = common.recvcs(sock)
       
   382         version = ord(cs.read(1))
       
   383 
       
   384         if version != common.version:
       
   385             self.ui.warn(_('received query from incompatible client '
       
   386                            'version %d\n') % version)
       
   387             try:
       
   388                 # try to send back our version to the client
       
   389                 # this way, the client too is informed of the mismatch
       
   390                 sock.sendall(chr(common.version))
       
   391             except socket.error:
       
   392                 pass
       
   393             return
       
   394 
       
   395         type = cs.read(4)
       
   396 
       
   397         if type == 'STAT':
       
   398             results = self.answer_stat_query(cs)
       
   399         elif type == 'DBUG':
       
   400             results = self.answer_dbug_query()
       
   401         else:
       
   402             self.ui.warn(_('unrecognized query type: %s\n') % type)
       
   403             return
       
   404 
       
   405         try:
       
   406             try:
       
   407                 v = chr(common.version)
       
   408 
       
   409                 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
       
   410                                             *map(len, results)))
       
   411                 sock.sendall(''.join(results))
       
   412             finally:
       
   413                 sock.shutdown(socket.SHUT_WR)
       
   414         except socket.error, err:
       
   415             if err.args[0] != errno.EPIPE:
       
   416                 raise
       
   417 
       
   418 if sys.platform.startswith('linux'):
       
   419     import linuxserver as _server
       
   420 else:
       
   421     raise ImportError
       
   422 
       
   423 master = _server.master
       
   424 
       
   425 def start(ui, dirstate, root, opts):
       
   426     timeout = opts.get('idle_timeout')
       
   427     if timeout:
       
   428         timeout = float(timeout) * 60000
       
   429     else:
       
   430         timeout = None
       
   431 
       
   432     class service(object):
       
   433         def init(self):
       
   434             try:
       
   435                 self.master = master(ui, dirstate, root, timeout)
       
   436             except AlreadyStartedException, inst:
       
   437                 raise util.Abort("inotify-server: %s" % inst)
       
   438 
       
   439         def run(self):
       
   440             try:
       
   441                 try:
       
   442                     self.master.run()
       
   443                 except TimeoutException:
       
   444                     pass
       
   445             finally:
       
   446                 self.master.shutdown()
       
   447 
       
   448     if 'inserve' not in sys.argv:
       
   449         runargs = util.hgcmd() + ['inserve', '-R', root]
       
   450     else:
       
   451         runargs = util.hgcmd() + sys.argv[1:]
       
   452 
       
   453     pidfile = ui.config('inotify', 'pidfile')
       
   454     opts.setdefault('pid_file', '')
       
   455     if opts['daemon'] and pidfile is not None and not opts['pid_file']:
       
   456         opts['pid_file'] = pidfile
       
   457 
       
   458     service = service()
       
   459     logfile = ui.config('inotify', 'log')
       
   460 
       
   461     appendpid = ui.configbool('inotify', 'appendpid', False)
       
   462 
       
   463     ui.debug('starting inotify server: %s\n' % ' '.join(runargs))
       
   464     cmdutil.service(opts, initfn=service.init, runfn=service.run,
       
   465                     logfile=logfile, runargs=runargs, appendpid=appendpid)