# 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()