hgext/inotify/linux/watcher.py
branchstable
changeset 21160 564f55b25122
parent 21028 a0f437e2f5a9
parent 21159 024f38f6d5f6
child 21161 ef59019f4771
equal deleted inserted replaced
21028:a0f437e2f5a9 21160:564f55b25122
     1 # watcher.py - high-level interfaces to the Linux inotify subsystem
       
     2 
       
     3 # Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
       
     4 
       
     5 # This library is free software; you can redistribute it and/or modify
       
     6 # it under the terms of version 2.1 of the GNU Lesser General Public
       
     7 # License, or any later version.
       
     8 
       
     9 '''High-level interfaces to the Linux inotify subsystem.
       
    10 
       
    11 The inotify subsystem provides an efficient mechanism for file status
       
    12 monitoring and change notification.
       
    13 
       
    14 The watcher class hides the low-level details of the inotify
       
    15 interface, and provides a Pythonic wrapper around it.  It generates
       
    16 events that provide somewhat more information than raw inotify makes
       
    17 available.
       
    18 
       
    19 The autowatcher class is more useful, as it automatically watches
       
    20 newly-created directories on your behalf.'''
       
    21 
       
    22 __author__ = "Bryan O'Sullivan <bos@serpentine.com>"
       
    23 
       
    24 import _inotify as inotify
       
    25 import array
       
    26 import errno
       
    27 import fcntl
       
    28 import os
       
    29 import termios
       
    30 
       
    31 
       
    32 class event(object):
       
    33     '''Derived inotify event class.
       
    34 
       
    35     The following fields are available:
       
    36 
       
    37         mask: event mask, indicating what kind of event this is
       
    38 
       
    39         cookie: rename cookie, if a rename-related event
       
    40 
       
    41         path: path of the directory in which the event occurred
       
    42 
       
    43         name: name of the directory entry to which the event occurred
       
    44         (may be None if the event happened to a watched directory)
       
    45 
       
    46         fullpath: complete path at which the event occurred
       
    47 
       
    48         wd: watch descriptor that triggered this event'''
       
    49 
       
    50     __slots__ = (
       
    51         'cookie',
       
    52         'fullpath',
       
    53         'mask',
       
    54         'name',
       
    55         'path',
       
    56         'raw',
       
    57         'wd',
       
    58         )
       
    59 
       
    60     def __init__(self, raw, path):
       
    61         self.path = path
       
    62         self.raw = raw
       
    63         if raw.name:
       
    64             self.fullpath = path + '/' + raw.name
       
    65         else:
       
    66             self.fullpath = path
       
    67 
       
    68         self.wd = raw.wd
       
    69         self.mask = raw.mask
       
    70         self.cookie = raw.cookie
       
    71         self.name = raw.name
       
    72 
       
    73     def __repr__(self):
       
    74         r = repr(self.raw)
       
    75         return 'event(path=' + repr(self.path) + ', ' + r[r.find('(') + 1:]
       
    76 
       
    77 
       
    78 _event_props = {
       
    79     'access': 'File was accessed',
       
    80     'modify': 'File was modified',
       
    81     'attrib': 'Attribute of a directory entry was changed',
       
    82     'close_write': 'File was closed after being written to',
       
    83     'close_nowrite': 'File was closed without being written to',
       
    84     'open': 'File was opened',
       
    85     'moved_from': 'Directory entry was renamed from this name',
       
    86     'moved_to': 'Directory entry was renamed to this name',
       
    87     'create': 'Directory entry was created',
       
    88     'delete': 'Directory entry was deleted',
       
    89     'delete_self': 'The watched directory entry was deleted',
       
    90     'move_self': 'The watched directory entry was renamed',
       
    91     'unmount': 'Directory was unmounted, and can no longer be watched',
       
    92     'q_overflow': 'Kernel dropped events due to queue overflow',
       
    93     'ignored': 'Directory entry is no longer being watched',
       
    94     'isdir': 'Event occurred on a directory',
       
    95     }
       
    96 
       
    97 for k, v in _event_props.iteritems():
       
    98     mask = getattr(inotify, 'IN_' + k.upper())
       
    99     def getter(self):
       
   100         return self.mask & mask
       
   101     getter.__name__ = k
       
   102     getter.__doc__ = v
       
   103     setattr(event, k, property(getter, doc=v))
       
   104 
       
   105 del _event_props
       
   106 
       
   107 
       
   108 class watcher(object):
       
   109     '''Provide a Pythonic interface to the low-level inotify API.
       
   110 
       
   111     Also adds derived information to each event that is not available
       
   112     through the normal inotify API, such as directory name.'''
       
   113 
       
   114     __slots__ = (
       
   115         'fd',
       
   116         '_paths',
       
   117         '_wds',
       
   118         )
       
   119 
       
   120     def __init__(self):
       
   121         '''Create a new inotify instance.'''
       
   122 
       
   123         self.fd = inotify.init()
       
   124         self._paths = {}
       
   125         self._wds = {}
       
   126 
       
   127     def fileno(self):
       
   128         '''Return the file descriptor this watcher uses.
       
   129 
       
   130         Useful for passing to select and poll.'''
       
   131 
       
   132         return self.fd
       
   133 
       
   134     def add(self, path, mask):
       
   135         '''Add or modify a watch.
       
   136 
       
   137         Return the watch descriptor added or modified.'''
       
   138 
       
   139         path = os.path.normpath(path)
       
   140         wd = inotify.add_watch(self.fd, path, mask)
       
   141         self._paths[path] = wd, mask
       
   142         self._wds[wd] = path, mask
       
   143         return wd
       
   144 
       
   145     def remove(self, wd):
       
   146         '''Remove the given watch.'''
       
   147 
       
   148         inotify.remove_watch(self.fd, wd)
       
   149         self._remove(wd)
       
   150 
       
   151     def _remove(self, wd):
       
   152         path_mask = self._wds.pop(wd, None)
       
   153         if path_mask is not None:
       
   154             self._paths.pop(path_mask[0])
       
   155 
       
   156     def path(self, path):
       
   157         '''Return a (watch descriptor, event mask) pair for the given path.
       
   158 
       
   159         If the path is not being watched, return None.'''
       
   160 
       
   161         return self._paths.get(path)
       
   162 
       
   163     def wd(self, wd):
       
   164         '''Return a (path, event mask) pair for the given watch descriptor.
       
   165 
       
   166         If the watch descriptor is not valid or not associated with
       
   167         this watcher, return None.'''
       
   168 
       
   169         return self._wds.get(wd)
       
   170 
       
   171     def read(self, bufsize=None):
       
   172         '''Read a list of queued inotify events.
       
   173 
       
   174         If bufsize is zero, only return those events that can be read
       
   175         immediately without blocking.  Otherwise, block until events are
       
   176         available.'''
       
   177 
       
   178         events = []
       
   179         for evt in inotify.read(self.fd, bufsize):
       
   180             events.append(event(evt, self._wds[evt.wd][0]))
       
   181             if evt.mask & inotify.IN_IGNORED:
       
   182                 self._remove(evt.wd)
       
   183             elif evt.mask & inotify.IN_UNMOUNT:
       
   184                 self.close()
       
   185         return events
       
   186 
       
   187     def close(self):
       
   188         '''Shut down this watcher.
       
   189 
       
   190         All subsequent method calls are likely to raise exceptions.'''
       
   191 
       
   192         os.close(self.fd)
       
   193         self.fd = None
       
   194         self._paths = None
       
   195         self._wds = None
       
   196 
       
   197     def __len__(self):
       
   198         '''Return the number of active watches.'''
       
   199 
       
   200         return len(self._paths)
       
   201 
       
   202     def __iter__(self):
       
   203         '''Yield a (path, watch descriptor, event mask) tuple for each
       
   204         entry being watched.'''
       
   205 
       
   206         for path, (wd, mask) in self._paths.iteritems():
       
   207             yield path, wd, mask
       
   208 
       
   209     def __del__(self):
       
   210         if self.fd is not None:
       
   211             os.close(self.fd)
       
   212 
       
   213     ignored_errors = [errno.ENOENT, errno.EPERM, errno.ENOTDIR]
       
   214 
       
   215     def add_iter(self, path, mask, onerror=None):
       
   216         '''Add or modify watches over path and its subdirectories.
       
   217 
       
   218         Yield each added or modified watch descriptor.
       
   219 
       
   220         To ensure that this method runs to completion, you must
       
   221         iterate over all of its results, even if you do not care what
       
   222         they are.  For example:
       
   223 
       
   224             for wd in w.add_iter(path, mask):
       
   225                 pass
       
   226 
       
   227         By default, errors are ignored.  If optional arg "onerror" is
       
   228         specified, it should be a function; it will be called with one
       
   229         argument, an OSError instance.  It can report the error to
       
   230         continue with the walk, or raise the exception to abort the
       
   231         walk.'''
       
   232 
       
   233         # Add the IN_ONLYDIR flag to the event mask, to avoid a possible
       
   234         # race when adding a subdirectory.  In the time between the
       
   235         # event being queued by the kernel and us processing it, the
       
   236         # directory may have been deleted, or replaced with a different
       
   237         # kind of entry with the same name.
       
   238 
       
   239         submask = mask | inotify.IN_ONLYDIR
       
   240 
       
   241         try:
       
   242             yield self.add(path, mask)
       
   243         except OSError, err:
       
   244             if onerror and err.errno not in self.ignored_errors:
       
   245                 onerror(err)
       
   246         for root, dirs, names in os.walk(path, topdown=False, onerror=onerror):
       
   247             for d in dirs:
       
   248                 try:
       
   249                     yield self.add(root + '/' + d, submask)
       
   250                 except OSError, err:
       
   251                     if onerror and err.errno not in self.ignored_errors:
       
   252                         onerror(err)
       
   253 
       
   254     def add_all(self, path, mask, onerror=None):
       
   255         '''Add or modify watches over path and its subdirectories.
       
   256 
       
   257         Return a list of added or modified watch descriptors.
       
   258 
       
   259         By default, errors are ignored.  If optional arg "onerror" is
       
   260         specified, it should be a function; it will be called with one
       
   261         argument, an OSError instance.  It can report the error to
       
   262         continue with the walk, or raise the exception to abort the
       
   263         walk.'''
       
   264 
       
   265         return [w for w in self.add_iter(path, mask, onerror)]
       
   266 
       
   267 
       
   268 class autowatcher(watcher):
       
   269     '''watcher class that automatically watches newly created directories.'''
       
   270 
       
   271     __slots__ = (
       
   272         'addfilter',
       
   273         )
       
   274 
       
   275     def __init__(self, addfilter=None):
       
   276         '''Create a new inotify instance.
       
   277 
       
   278         This instance will automatically watch newly created
       
   279         directories.
       
   280 
       
   281         If the optional addfilter parameter is not None, it must be a
       
   282         callable that takes one parameter.  It will be called each time
       
   283         a directory is about to be automatically watched.  If it returns
       
   284         True, the directory will be watched if it still exists,
       
   285         otherwise, it will be skipped.'''
       
   286 
       
   287         super(autowatcher, self).__init__()
       
   288         self.addfilter = addfilter
       
   289 
       
   290     _dir_create_mask = inotify.IN_ISDIR | inotify.IN_CREATE
       
   291 
       
   292     def read(self, bufsize=None):
       
   293         events = super(autowatcher, self).read(bufsize)
       
   294         for evt in events:
       
   295             if evt.mask & self._dir_create_mask == self._dir_create_mask:
       
   296                 if self.addfilter is None or self.addfilter(evt):
       
   297                     parentmask = self._wds[evt.wd][1]
       
   298                     # See note about race avoidance via IN_ONLYDIR above.
       
   299                     mask = parentmask | inotify.IN_ONLYDIR
       
   300                     try:
       
   301                         self.add_all(evt.fullpath, mask)
       
   302                     except OSError, err:
       
   303                         if err.errno not in self.ignored_errors:
       
   304                             raise
       
   305         return events
       
   306 
       
   307 
       
   308 class threshold(object):
       
   309     '''Class that indicates whether a file descriptor has reached a
       
   310     threshold of readable bytes available.
       
   311 
       
   312     This class is not thread-safe.'''
       
   313 
       
   314     __slots__ = (
       
   315         'fd',
       
   316         'threshold',
       
   317         '_iocbuf',
       
   318         )
       
   319 
       
   320     def __init__(self, fd, threshold=1024):
       
   321         self.fd = fd
       
   322         self.threshold = threshold
       
   323         self._iocbuf = array.array('i', [0])
       
   324 
       
   325     def readable(self):
       
   326         '''Return the number of bytes readable on this file descriptor.'''
       
   327 
       
   328         fcntl.ioctl(self.fd, termios.FIONREAD, self._iocbuf, True)
       
   329         return self._iocbuf[0]
       
   330 
       
   331     def __call__(self):
       
   332         '''Indicate whether the number of readable bytes has met or
       
   333         exceeded the threshold.'''
       
   334 
       
   335         return self.readable() >= self.threshold