Before I introduce the code, let me say that this is not a 100% exact implementation of the interfaces that can be found in pyinotify but the implementation of a subset that matches my needs. The main idea of creating this post is to give an example of the implementation of such a library for Windows trying to reuse the code that can be found in pyinotify.
Once I have excused my self, let get into the code. First of all, there are a number of classes from pyinotify that we can use in our code. That subset of classes is the below code which I grabbed from pyinotify git:
#!/usr/bin/env python # pyinotify.py - python interface to inotify # Copyright (c) 2010 Sebastien Martini <seb@dbzteam.org> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """Platform agnostic code grabed from pyinotify.""" import logging import os COMPATIBILITY_MODE = False class RawOutputFormat: """ Format string representations. """ def __init__(self, format=None): self.format = format or {} def simple(self, s, attribute): if not isinstance(s, str): s = str(s) return (self.format.get(attribute, '') + s + self.format.get('normal', '')) def punctuation(self, s): """Punctuation color.""" return self.simple(s, 'normal') def field_value(self, s): """Field value color.""" return self.simple(s, 'purple') def field_name(self, s): """Field name color.""" return self.simple(s, 'blue') def class_name(self, s): """Class name color.""" return self.format.get('red', '') + self.simple(s, 'bold') output_format = RawOutputFormat() class EventsCodes: """ Set of codes corresponding to each kind of events. Some of these flags are used to communicate with inotify, whereas the others are sent to userspace by inotify notifying some events. @cvar IN_ACCESS: File was accessed. @type IN_ACCESS: int @cvar IN_MODIFY: File was modified. @type IN_MODIFY: int @cvar IN_ATTRIB: Metadata changed. @type IN_ATTRIB: int @cvar IN_CLOSE_WRITE: Writtable file was closed. @type IN_CLOSE_WRITE: int @cvar IN_CLOSE_NOWRITE: Unwrittable file closed. @type IN_CLOSE_NOWRITE: int @cvar IN_OPEN: File was opened. @type IN_OPEN: int @cvar IN_MOVED_FROM: File was moved from X. @type IN_MOVED_FROM: int @cvar IN_MOVED_TO: File was moved to Y. @type IN_MOVED_TO: int @cvar IN_CREATE: Subfile was created. @type IN_CREATE: int @cvar IN_DELETE: Subfile was deleted. @type IN_DELETE: int @cvar IN_DELETE_SELF: Self (watched item itself) was deleted. @type IN_DELETE_SELF: int @cvar IN_MOVE_SELF: Self (watched item itself) was moved. @type IN_MOVE_SELF: int @cvar IN_UNMOUNT: Backing fs was unmounted. @type IN_UNMOUNT: int @cvar IN_Q_OVERFLOW: Event queued overflowed. @type IN_Q_OVERFLOW: int @cvar IN_IGNORED: File was ignored. @type IN_IGNORED: int @cvar IN_ONLYDIR: only watch the path if it is a directory (new in kernel 2.6.15). @type IN_ONLYDIR: int @cvar IN_DONT_FOLLOW: don't follow a symlink (new in kernel 2.6.15). IN_ONLYDIR we can make sure that we don't watch the target of symlinks. @type IN_DONT_FOLLOW: int @cvar IN_MASK_ADD: add to the mask of an already existing watch (new in kernel 2.6.14). @type IN_MASK_ADD: int @cvar IN_ISDIR: Event occurred against dir. @type IN_ISDIR: int @cvar IN_ONESHOT: Only send event once. @type IN_ONESHOT: int @cvar ALL_EVENTS: Alias for considering all of the events. @type ALL_EVENTS: int """ # The idea here is 'configuration-as-code' - this way, we get # our nice class constants, but we also get nice human-friendly text # mappings to do lookups against as well, for free: FLAG_COLLECTIONS = {'OP_FLAGS': { 'IN_ACCESS' : 0x00000001, # File was accessed 'IN_MODIFY' : 0x00000002, # File was modified 'IN_ATTRIB' : 0x00000004, # Metadata changed 'IN_CLOSE_WRITE' : 0x00000008, # Writable file was closed 'IN_CLOSE_NOWRITE' : 0x00000010, # Unwritable file closed 'IN_OPEN' : 0x00000020, # File was opened 'IN_MOVED_FROM' : 0x00000040, # File was moved from X 'IN_MOVED_TO' : 0x00000080, # File was moved to Y 'IN_CREATE' : 0x00000100, # Subfile was created 'IN_DELETE' : 0x00000200, # Subfile was deleted 'IN_DELETE_SELF' : 0x00000400, # Self (watched item itself) # was deleted 'IN_MOVE_SELF' : 0x00000800, # Self(watched item itself) was moved }, 'EVENT_FLAGS': { 'IN_UNMOUNT' : 0x00002000, # Backing fs was unmounted 'IN_Q_OVERFLOW' : 0x00004000, # Event queued overflowed 'IN_IGNORED' : 0x00008000, # File was ignored }, 'SPECIAL_FLAGS': { 'IN_ONLYDIR' : 0x01000000, # only watch the path if it is a # directory 'IN_DONT_FOLLOW' : 0x02000000, # don't follow a symlink 'IN_MASK_ADD' : 0x20000000, # add to the mask of an already # existing watch 'IN_ISDIR' : 0x40000000, # event occurred against dir 'IN_ONESHOT' : 0x80000000, # only send event once }, } def maskname(mask): """ Returns the event name associated to mask. IN_ISDIR is appended to the result when appropriate. Note: only one event is returned, because only one event can be raised at a given time. @param mask: mask. @type mask: int @return: event name. @rtype: str """ ms = mask name = '%s' if mask & IN_ISDIR: ms = mask - IN_ISDIR name = '%s|IN_ISDIR' return name % EventsCodes.ALL_VALUES[ms] maskname = staticmethod(maskname) # So let's now turn the configuration into code EventsCodes.ALL_FLAGS = {} EventsCodes.ALL_VALUES = {} for flagc, valc in EventsCodes.FLAG_COLLECTIONS.items(): # Make the collections' members directly accessible through the # class dictionary setattr(EventsCodes, flagc, valc) # Collect all the flags under a common umbrella EventsCodes.ALL_FLAGS.update(valc) # Make the individual masks accessible as 'constants' at globals() scope # and masknames accessible by values. for name, val in valc.items(): globals()[name] = val EventsCodes.ALL_VALUES[val] = name # all 'normal' events ALL_EVENTS = reduce(lambda x, y: x | y, EventsCodes.OP_FLAGS.values()) EventsCodes.ALL_FLAGS['ALL_EVENTS'] = ALL_EVENTS EventsCodes.ALL_VALUES[ALL_EVENTS] = 'ALL_EVENTS' class _Event: """ Event structure, represent events raised by the system. This is the base class and should be subclassed. """ def __init__(self, dict_): """ Attach attributes (contained in dict_) to self. @param dict_: Set of attributes. @type dict_: dictionary """ for tpl in dict_.items(): setattr(self, *tpl) def __repr__(self): """ @return: Generic event string representation. @rtype: str """ s = '' for attr, value in sorted(self.__dict__.items(), key=lambda x: x[0]): if attr.startswith('_'): continue if attr == 'mask': value = hex(getattr(self, attr)) elif isinstance(value, basestring) and not value: value = "''" s += ' %s%s%s' % (output_format.field_name(attr), output_format.punctuation('='), output_format.field_value(value)) s = '%s%s%s %s' % (output_format.punctuation('<'), output_format.class_name(self.__class__.__name__), s, output_format.punctuation('>')) return s def __str__(self): return repr(self) class _RawEvent(_Event): """ Raw event, it contains only the informations provided by the system. It doesn't infer anything. """ def __init__(self, wd, mask, cookie, name): """ @param wd: Watch Descriptor. @type wd: int @param mask: Bitmask of events. @type mask: int @param cookie: Cookie. @type cookie: int @param name: Basename of the file or directory against which the event was raised in case where the watched directory is the parent directory. None if the event was raised on the watched item itself. @type name: string or None """ # Use this variable to cache the result of str(self), this object # is immutable. self._str = None # name: remove trailing '\0' d = {'wd': wd, 'mask': mask, 'cookie': cookie, 'name': name.rstrip('\0')} _Event.__init__(self, d) logging.debug(str(self)) def __str__(self): if self._str is None: self._str = _Event.__str__(self) return self._str class Event(_Event): """ This class contains all the useful informations about the observed event. However, the presence of each field is not guaranteed and depends on the type of event. In effect, some fields are irrelevant for some kind of event (for example 'cookie' is meaningless for IN_CREATE whereas it is mandatory for IN_MOVE_TO). The possible fields are: - wd (int): Watch Descriptor. - mask (int): Mask. - maskname (str): Readable event name. - path (str): path of the file or directory being watched. - name (str): Basename of the file or directory against which the event was raised in case where the watched directory is the parent directory. None if the event was raised on the watched item itself. This field is always provided even if the string is ''. - pathname (str): Concatenation of 'path' and 'name'. - src_pathname (str): Only present for IN_MOVED_TO events and only in the case where IN_MOVED_FROM events are watched too. Holds the source pathname from where pathname was moved from. - cookie (int): Cookie. - dir (bool): True if the event was raised against a directory. """ def __init__(self, raw): """ Concretely, this is the raw event plus inferred infos. """ _Event.__init__(self, raw) self.maskname = EventsCodes.maskname(self.mask) if COMPATIBILITY_MODE: self.event_name = self.maskname try: if self.name: self.pathname = os.path.abspath(os.path.join(self.path, self.name)) else: self.pathname = os.path.abspath(self.path) except AttributeError, err: # Usually it is not an error some events are perfectly valids # despite the lack of these attributes. logging.debug(err) class _ProcessEvent: """ Abstract processing event class. """ def __call__(self, event): """ To behave like a functor the object must be callable. This method is a dispatch method. Its lookup order is: 1. process_MASKNAME method 2. process_FAMILY_NAME method 3. otherwise calls process_default @param event: Event to be processed. @type event: Event object @return: By convention when used from the ProcessEvent class: - Returning False or None (default value) means keep on executing next chained functors (see chain.py example). - Returning True instead means do not execute next processing functions. @rtype: bool @raise ProcessEventError: Event object undispatchable, unknown event. """ stripped_mask = event.mask - (event.mask & IN_ISDIR) maskname = EventsCodes.ALL_VALUES.get(stripped_mask) if maskname is None: raise ProcessEventError("Unknown mask 0x%08x" % stripped_mask) # 1- look for process_MASKNAME meth = getattr(self, 'process_' + maskname, None) if meth is not None: return meth(event) # 2- look for process_FAMILY_NAME meth = getattr(self, 'process_IN_' + maskname.split('_')[1], None) if meth is not None: return meth(event) # 3- default call method process_default return self.process_default(event) def __repr__(self): return '<%s>' % self.__class__.__name__ class ProcessEvent(_ProcessEvent): """ Process events objects, can be specialized via subclassing, thus its behavior can be overriden: Note: you should not override __init__ in your subclass instead define a my_init() method, this method will be called automatically from the constructor of this class with its optionals parameters. 1. Provide specialized individual methods, e.g. process_IN_DELETE for processing a precise type of event (e.g. IN_DELETE in this case). 2. Or/and provide methods for processing events by 'family', e.g. process_IN_CLOSE method will process both IN_CLOSE_WRITE and IN_CLOSE_NOWRITE events (if process_IN_CLOSE_WRITE and process_IN_CLOSE_NOWRITE aren't defined though). 3. Or/and override process_default for catching and processing all the remaining types of events. """ pevent = None def __init__(self, pevent=None, **kargs): """ Enable chaining of ProcessEvent instances. @param pevent: Optional callable object, will be called on event processing (before self). @type pevent: callable @param kargs: This constructor is implemented as a template method delegating its optionals keyworded arguments to the method my_init(). @type kargs: dict """ self.pevent = pevent self.my_init(**kargs) def my_init(self, **kargs): """ This method is called from ProcessEvent.__init__(). This method is empty here and must be redefined to be useful. In effect, if you need to specifically initialize your subclass' instance then you just have to override this method in your subclass. Then all the keyworded arguments passed to ProcessEvent.__init__() will be transmitted as parameters to this method. Beware you MUST pass keyword arguments though. @param kargs: optional delegated arguments from __init__(). @type kargs: dict """ pass def __call__(self, event): stop_chaining = False if self.pevent is not None: # By default methods return None so we set as guideline # that methods asking for stop chaining must explicitely # return non None or non False values, otherwise the default # behavior will be to accept chain call to the corresponding # local method. stop_chaining = self.pevent(event) if not stop_chaining: return _ProcessEvent.__call__(self, event) def nested_pevent(self): return self.pevent def process_IN_Q_OVERFLOW(self, event): """ By default this method only reports warning messages, you can overredide it by subclassing ProcessEvent and implement your own process_IN_Q_OVERFLOW method. The actions you can take on receiving this event is either to update the variable max_queued_events in order to handle more simultaneous events or to modify your code in order to accomplish a better filtering diminishing the number of raised events. Because this method is defined, IN_Q_OVERFLOW will never get transmitted as arguments to process_default calls. @param event: IN_Q_OVERFLOW event. @type event: dict """ log.warning('Event queue overflowed.') def process_default(self, event): """ Default processing event method. By default does nothing. Subclass ProcessEvent and redefine this method in order to modify its behavior. @param event: Event to be processed. Can be of any type of events but IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW). @type event: Event instance """ pass class PrintAllEvents(ProcessEvent): """ Dummy class used to print events strings representations. For instance this class is used from command line to print all received events to stdout. """ def my_init(self, out=None): """ @param out: Where events will be written. @type out: Object providing a valid file object interface. """ if out is None: out = sys.stdout self._out = out def process_default(self, event): """ Writes event string representation to file object provided to my_init(). @param event: Event to be processed. Can be of any type of events but IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW). @type event: Event instance """ self._out.write(str(event)) self._out.write('\n') self._out.flush() class WatchManagerError(Exception): """ WatchManager Exception. Raised on error encountered on watches operations. """ def __init__(self, msg, wmd): """ @param msg: Exception string's description. @type msg: string @param wmd: This dictionary contains the wd assigned to paths of the same call for which watches were successfully added. @type wmd: dict """ self.wmd = wmd Exception.__init__(self, msg)
Unfortunatly we need to implement the code that talks with the Win32 API to be able to retrieve the events in the file system. In my design this is done by the Watch class that looks like this:
# Author: Manuel de la Pena <manuel@canonical.com> # # Copyright 2011 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see <http://www.gnu.org/licenses/>. """File notifications on windows.""" import logging import os import re import winerror from Queue import Queue, Empty from threading import Thread from uuid import uuid4 from twisted.internet import task, reactor from win32con import ( FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_FLAG_BACKUP_SEMANTICS, FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY, OPEN_EXISTING ) from win32file import CreateFile, ReadDirectoryChangesW from ubuntuone.platform.windows.pyinotify import ( Event, WatchManagerError, ProcessEvent, PrintAllEvents, IN_OPEN, IN_CLOSE_NOWRITE, IN_CLOSE_WRITE, IN_CREATE, IN_ISDIR, IN_DELETE, IN_MOVED_FROM, IN_MOVED_TO, IN_MODIFY, IN_IGNORED ) from ubuntuone.syncdaemon.filesystem_notifications import ( GeneralINotifyProcessor ) from ubuntuone.platform.windows.os_helper import ( LONG_PATH_PREFIX, abspath, listdir ) # constant found in the msdn documentation: # http://msdn.microsoft.com/en-us/library/ff538834(v=vs.85).aspx FILE_LIST_DIRECTORY = 0x0001 FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020 FILE_NOTIFY_CHANGE_CREATION = 0x00000040 # a map between the few events that we have on windows and those # found in pyinotify WINDOWS_ACTIONS = { 1: IN_CREATE, 2: IN_DELETE, 3: IN_MODIFY, 4: IN_MOVED_FROM, 5: IN_MOVED_TO } # translates quickly the event and it's is_dir state to our standard events NAME_TRANSLATIONS = { IN_OPEN: 'FS_FILE_OPEN', IN_CLOSE_NOWRITE: 'FS_FILE_CLOSE_NOWRITE', IN_CLOSE_WRITE: 'FS_FILE_CLOSE_WRITE', IN_CREATE: 'FS_FILE_CREATE', IN_CREATE | IN_ISDIR: 'FS_DIR_CREATE', IN_DELETE: 'FS_FILE_DELETE', IN_DELETE | IN_ISDIR: 'FS_DIR_DELETE', IN_MOVED_FROM: 'FS_FILE_DELETE', IN_MOVED_FROM | IN_ISDIR: 'FS_DIR_DELETE', IN_MOVED_TO: 'FS_FILE_CREATE', IN_MOVED_TO | IN_ISDIR: 'FS_DIR_CREATE', } # the default mask to be used in the watches added by the FilesystemMonitor # class FILESYSTEM_MONITOR_MASK = FILE_NOTIFY_CHANGE_FILE_NAME | \ FILE_NOTIFY_CHANGE_DIR_NAME | \ FILE_NOTIFY_CHANGE_ATTRIBUTES | \ FILE_NOTIFY_CHANGE_SIZE | \ FILE_NOTIFY_CHANGE_LAST_WRITE | \ FILE_NOTIFY_CHANGE_SECURITY | \ FILE_NOTIFY_CHANGE_LAST_ACCESS # The implementation of the code that is provided as the pyinotify # substitute class Watch(object): """Implement the same functions as pyinotify.Watch.""" def __init__(self, watch_descriptor, path, mask, auto_add, events_queue=None, exclude_filter=None, proc_fun=None): super(Watch, self).__init__() self.log = logging.getLogger('ubuntuone.platform.windows.' + 'filesystem_notifications.Watch') self._watching = False self._descriptor = watch_descriptor self._auto_add = auto_add self.exclude_filter = None self._proc_fun = proc_fun self._cookie = None self._source_pathname = None # remember the subdirs we have so that when we have a delete we can # check if it was a remove self._subdirs = [] # ensure that we work with an abspath and that we can deal with # long paths over 260 chars. self._path = os.path.abspath(path) if not self._path.startswith(LONG_PATH_PREFIX): self._path = LONG_PATH_PREFIX + self._path self._mask = mask # lets make the q as big as possible self._raw_events_queue = Queue() if not events_queue: events_queue = Queue() self.events_queue = events_queue def _path_is_dir(self, path): """"Check if the path is a dir and update the local subdir list.""" self.log.debug('Testing if path "%s" is a dir', path) is_dir = False if os.path.exists(path): is_dir = os.path.isdir(path) else: self.log.debug('Path "%s" was deleted subdirs are %s.', path, self._subdirs) # we removed the path, we look in the internal list if path in self._subdirs: is_dir = True self._subdirs.remove(path) if is_dir: self.log.debug('Adding %s to subdirs %s', path, self._subdirs) self._subdirs.append(path) return is_dir def _process_events(self): """Process the events form the queue.""" # we transform the events to be the same as the one in pyinotify # and then use the proc_fun while self._watching or not self._raw_events_queue.empty(): file_name, action = self._raw_events_queue.get() # map the windows events to the pyinotify ones, tis is dirty but # makes the multiplatform better, linux was first :P is_dir = self._path_is_dir(file_name) if os.path.exists(file_name): is_dir = os.path.isdir(file_name) else: # we removed the path, we look in the internal list if file_name in self._subdirs: is_dir = True self._subdirs.remove(file_name) if is_dir: self._subdirs.append(file_name) mask = WINDOWS_ACTIONS[action] head, tail = os.path.split(file_name) if is_dir: mask |= IN_ISDIR event_raw_data = { 'wd': self._descriptor, 'dir': is_dir, 'mask': mask, 'name': tail, 'path': head.replace(self.path, '.') } # by the way in which the win api fires the events we know for # sure that no move events will be added in the wrong order, this # is kind of hacky, I dont like it too much if WINDOWS_ACTIONS[action] == IN_MOVED_FROM: self._cookie = str(uuid4()) self._source_pathname = tail event_raw_data['cookie'] = self._cookie if WINDOWS_ACTIONS[action] == IN_MOVED_TO: event_raw_data['src_pathname'] = self._source_pathname event_raw_data['cookie'] = self._cookie event = Event(event_raw_data) # FIXME: event deduces the pathname wrong and we need manually # set it event.pathname = file_name # add the event only if we do not have an exclude filter or # the exclude filter returns False, that is, the event will not # be excluded if not self.exclude_filter or not self.exclude_filter(event): self.log.debug('Addding event %s to queue.', event) self.events_queue.put(event) def _watch(self): """Watch a path that is a directory.""" # we are going to be using the ReadDirectoryChangesW whihc requires # a direcotry handle and the mask to be used. handle = CreateFile( self._path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, None ) self.log.debug('Watchng path %s.', self._path) while self._watching: # important information to know about the parameters: # param 1: the handle to the dir # param 2: the size to be used in the kernel to store events # that might be lost whilw the call is being performed. This # is complicates to fine tune since if you make lots of watcher # you migh used to much memory and make your OS to BSOD results = ReadDirectoryChangesW( handle, 1024, self._auto_add, self._mask, None, None ) # add the diff events to the q so that the can be processed no # matter the speed. for action, file in results: full_filename = os.path.join(self._path, file) self._raw_events_queue.put((full_filename, action)) self.log.debug('Added %s to raw events queue.', (full_filename, action)) def start_watching(self): """Tell the watch to start processing events.""" # get the diff dirs in the path for current_child in listdir(self._path): full_child_path = os.path.join(self._path, current_child) if os.path.isdir(full_child_path): self._subdirs.append(full_child_path) # start to diff threads, one to watch the path, the other to # process the events. self.log.debug('Sart watching path.') self._watching = True watch_thread = Thread(target=self._watch, name='Watch(%s)' % self._path) process_thread = Thread(target=self._process_events, name='Process(%s)' % self._path) process_thread.start() watch_thread.start() def stop_watching(self): """Tell the watch to stop processing events.""" self._watching = False self._subdirs = [] def update(self, mask, proc_fun=None, auto_add=False): """Update the info used by the watcher.""" self.log.debug('update(%s, %s, %s)', mask, proc_fun, auto_add) self._mask = mask self._proc_fun = proc_fun self._auto_add = auto_add @property def path(self): """Return the patch watched.""" return self._path @property def auto_add(self): return self._auto_add @property def proc_fun(self): return self._proc_fun class WatchManager(object): """Implement the same functions as pyinotify.WatchManager.""" def __init__(self, exclude_filter=lambda path: False): """Init the manager to keep trak of the different watches.""" super(WatchManager, self).__init__() self.log = logging.getLogger('ubuntuone.platform.windows.' + 'filesystem_notifications.WatchManager') self._wdm = {} self._wd_count = 0 self._exclude_filter = exclude_filter self._events_queue = Queue() self._ignored_paths = [] def stop(self): """Close the manager and stop all watches.""" self.log.debug('Stopping watches.') for current_wd in self._wdm: self._wdm[current_wd].stop_watching() self.log.debug('Watch for %s stopped.', self._wdm[current_wd].path) def get_watch(self, wd): """Return the watch with the given descriptor.""" return self._wdm[wd] def del_watch(self, wd): """Delete the watch with the given descriptor.""" try: watch = self._wdm[wd] watch.stop_watching() del self._wdm[wd] self.log.debug('Watch %s removed.', wd) except KeyError, e: logging.error(str(e)) def _add_single_watch(self, path, mask, proc_fun=None, auto_add=False, quiet=True, exclude_filter=None): self.log.debug('add_single_watch(%s, %s, %s, %s, %s, %s)', path, mask, proc_fun, auto_add, quiet, exclude_filter) self._wdm[self._wd_count] = Watch(self._wd_count, path, mask, auto_add, events_queue=self._events_queue, exclude_filter=exclude_filter, proc_fun=proc_fun) self._wdm[self._wd_count].start_watching() self._wd_count += 1 self.log.debug('Watch count increased to %s', self._wd_count) def add_watch(self, path, mask, proc_fun=None, auto_add=False, quiet=True, exclude_filter=None): if hasattr(path, '__iter__'): self.log.debug('Added collection of watches.') # we are dealing with a collection of paths for current_path in path: if not self.get_wd(current_path): self._add_single_watch(current_path, mask, proc_fun, auto_add, quiet, exclude_filter) elif not self.get_wd(path): self.log.debug('Adding single watch.') self._add_single_watch(path, mask, proc_fun, auto_add, quiet, exclude_filter) def update_watch(self, wd, mask=None, proc_fun=None, rec=False, auto_add=False, quiet=True): try: watch = self._wdm[wd] watch.stop_watching() self.log.debug('Stopped watch on %s for update.', watch.path) # update the data and restart watching auto_add = auto_add or rec watch.update(mask, proc_fun=proc_fun, auto_add=auto_add) # only start the watcher again if the mask was given, otherwhise # we are not watchng and therefore do not care if mask: watch.start_watching() except KeyError, e: self.log.error(str(e)) if not quiet: raise WatchManagerError('Watch %s was not found' % wd, {}) def get_wd(self, path): """Return the watcher that is used to watch the given path.""" for current_wd in self._wdm: if self._wdm[current_wd].path in path and \ self._wdm[current_wd].auto_add: return current_wd def get_path(self, wd): """Return the path watched by the wath with the given wd.""" watch_ = self._wmd.get(wd) if watch: return watch.path def rm_watch(self, wd, rec=False, quiet=True): """Remove the the watch with the given wd.""" try: watch = self._wdm[wd] watch.stop_watching() del self._wdm[wd] except KeyrError, err: self.log.error(str(err)) if not quiet: raise WatchManagerError('Watch %s was not found' % wd, {}) def rm_path(self, path): """Remove a watch to the given path.""" # it would be very tricky to remove a subpath from a watcher that is # looking at changes in ther kids. To make it simpler and less error # prone (and even better performant since we use less threads) we will # add a filter to the events in the watcher so that the events from # that child are not received :) def ignore_path(event): """Ignore an event if it has a given path.""" is_ignored = False for ignored_path in self._ignored_paths: if ignore_path in event.pathname: return True return False wd = self.get_wd(path) if wd: if self._wdm[wd].path == path: self.log.debug('Removing watch for path "%s"', path) self.rm_watch(wd) else: self.log.debug('Adding exclude filter for "%s"', path) # we have a watch that cotains the path as a child path if not path in self._ignored_paths: self._ignored_paths.append(path) # FIXME: This assumes that we do not have other function # which in our usecase is correct, but what is we move this # to other projects evet?!? Maybe using the manager # exclude_filter is better if not self._wdm[wd].exclude_filter: self._wdm[wd].exclude_filter = ignore_path @property def watches(self): """Return a reference to the dictionary that contains the watches.""" return self._wdm @property def events_queue(self): """Return the queue with the events that the manager contains.""" return self._events_queue class Notifier(object): """ Read notifications, process events. Inspired by the pyinotify.Notifier """ def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=10, timeout=-1): """Init to process event according to the given timeout & threshold.""" super(Notifier, self).__init__() self.log = logging.getLogger('ubuntuone.platform.windows.' + 'filesystem_notifications.Notifier') # Watch Manager instance self._watch_manager = watch_manager # Default processing method self._default_proc_fun = default_proc_fun if default_proc_fun is None: self._default_proc_fun = PrintAllEvents() # Loop parameters self._read_freq = read_freq self._threshold = threshold self._timeout = timeout def proc_fun(self): return self._default_proc_fun def process_events(self): """ Process the event given the threshold and the timeout. """ self.log.debug('Processing events with threashold: %s and timeout: %s', self._threshold, self._timeout) # we will process an amount of events equal to the threshold of # the notifier and will block for the amount given by the timeout processed_events = 0 while processed_events < self._threshold: try: raw_event = None if not self._timeout or self._timeout < 0: raw_event = self._watch_manager.events_queue.get( block=False) else: raw_event = self._watch_manager.events_queue.get( timeout=self._timeout) watch = self._watch_manager.get_watch(raw_event.wd) if watch is None: # Not really sure how we ended up here, nor how we should # handle these types of events and if it is appropriate to # completly skip them (like we are doing here). self.log.warning('Unable to retrieve Watch object ' + 'associated to %s', raw_event) processed_events += 1 continue if watch and watch.proc_fun: self.log.debug('Executing proc_fun from watch.') watch.proc_fun(raw_event) # user processings else: self.log.debug('Executing default_proc_fun') self._default_proc_fun(raw_event) processed_events += 1 except Empty: # increase the number of processed events, and continue processed_events += 1 continue def stop(self): """Stop processing events and the watch manager.""" self._watch_manager.stop()
While one of the threads is retrieving the events from the file system, the second one process them so that the will be exposed as pyinotify events. I have done so because I did not want to deal with OVERLAP structures for asyn operations in Win32 and because I wanted to use pyinotify events so that if someone with experience in pyinotify looks at the output, he can easily understand it. I really like this approach because it allowed me to reuse a fair amount of logic hat we had in the Ubuntu client and to approach the port in a very TDD way since the tests I’ve used are the same ones as the ones found on Ubuntu
Latest Official Posts