Merge lp:~diegosarmentero/ubuntuone-client/darwin2-fsevents into lp:ubuntuone-client

Proposed by Diego Sarmentero on 2012-06-21
Status: Merged
Approved by: Alejandro J. Cura on 2012-06-26
Approved revision: 1268
Merged at revision: 1267
Proposed branch: lp:~diegosarmentero/ubuntuone-client/darwin2-fsevents
Merge into: lp:ubuntuone-client
Prerequisite: lp:~diegosarmentero/ubuntuone-client/darwin-fsevents-1
Diff against target: 912 lines (+376/-280)
4 files modified
tests/platform/filesystem_notifications/test_windows.py (+3/-3)
ubuntuone/platform/filesystem_notifications/__init__.py (+1/-0)
ubuntuone/platform/filesystem_notifications/common.py (+64/-277)
ubuntuone/platform/filesystem_notifications/windows.py (+308/-0)
To merge this branch: bzr merge lp:~diegosarmentero/ubuntuone-client/darwin2-fsevents
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) 2012-06-21 Approve on 2012-06-26
Manuel de la Peña (community) Approve on 2012-06-25
Review via email: mp+111427@code.launchpad.net

Commit Message

- Refactoring windows fsevents implementation, to be usable from the future darwin one (LP: #1013323).

To post a comment you must log in.
Alejandro J. Cura (alecu) wrote :

The implementation in add_watches_to_udf_ancestors is *very* specific to windows, so it needs to be moved to windows.py

---

FILESYSTEM_MONITOR_MASK gets broken in this branch because filesystem_monitor_mask seems to always be None.

---

All of this would break on darwin:
    is_valid_syncdaemon_path = None
    is_valid_os_path = None
    os_path = windowspath = None

So everything under:
   elif sys.platform == "darwin":

should come on a following branch.

---

Please rename "reference" to something less vague, and move each definition to the platform specific files.

review: Needs Fixing
1263. By Diego Sarmentero on 2012-06-22

Updating branch

Diego Sarmentero (diegosarmentero) wrote :

> The implementation in add_watches_to_udf_ancestors is *very* specific to
> windows, so it needs to be moved to windows.py
>

Moved to windows, we will need to implement later for darwin something here, but not just returning True as in windows.

> ---
>
> FILESYSTEM_MONITOR_MASK gets broken in this branch because
> filesystem_monitor_mask seems to always be None.
>

This is not broken, the mask is assigned in windows.py, and for darwin i'm using it as None, so i can share more common code in common.py.

> ---
>
> All of this would break on darwin:
> is_valid_syncdaemon_path = None
> is_valid_os_path = None
> os_path = windowspath = None
>
> So everything under:
> elif sys.platform == "darwin":
>
> should come on a following branch.
>
> ---
>
> Please rename "reference" to something less vague, and move each definition to
> the platform specific files.

Done

Alejandro J. Cura (alecu) wrote :

> > FILESYSTEM_MONITOR_MASK gets broken in this branch because
> > filesystem_monitor_mask seems to always be None.
> >
>
> This is not broken, the mask is assigned in windows.py, and for darwin i'm
> using it as None, so i can share more common code in common.py.

You are right! Sorry about that :P

Alejandro J. Cura (alecu) wrote :

This comment is *Very* specific to windows:

        # On windows there is no need to add the watches to the ancestors
        # so we will always return true. The reason for this is that the
        # handles that we open stop the user from renaming the ancestors of
        # the UDF, for a user to do that he has to unsync the udf first

This too:
    For Windows:
    Also, they must not be literal paths, that is the \\?\ prefix should not be
    in the path.

Both should be in windows.py.

review: Needs Fixing
1264. By Diego Sarmentero on 2012-06-22

merge

1265. By Diego Sarmentero on 2012-06-22

updates

1266. By Diego Sarmentero on 2012-06-22

adding not implemented error

1267. By Diego Sarmentero on 2012-06-22

moving event codes to filesystem notification init

Diego Sarmentero (diegosarmentero) wrote :

Branch fixed.
I added the event codes in the filesystem_notifications/__init__.py so we can create where it actually does this os dependent thing, and we can grab it from anywhere, because implementing it as a class attribute was breaking a lot of things.

Also i remove the if sys.platform == "win32": import decorators
on common, and in the future branch, when i add the darwin part, i'll make os_helper import the proper decorator based on the platform as it should be.

review: Approve
Alejandro J. Cura (alecu) wrote :

This will probably break on darwin, where fs paths are utf-8 bytes, so it should go from common.py to windows.py:

473 if not isinstance(path, unicode):

----

The comment that starts with:
        # We are using this on windows and darwin.
        # For windows the logic is as follow:
        # ....

is still crap. The whole 9 lines of it, so it's not completely your fault, but it's crap.
Let's rewrite it with something simpler like this:

# We need to manually check if the path is a folder, because
# neither ReadDirectoryChangesW nor the FSEvents API tell us

Also the docstring in that function lies about "update the local subdir list".

---

review: Needs Fixing
1268. By Diego Sarmentero on 2012-06-26

fixing comment

Diego Sarmentero (diegosarmentero) wrote :

> This will probably break on darwin, where fs paths are utf-8 bytes, so it
> should go from common.py to windows.py:
>
> 473 if not isinstance(path, unicode):
>
> ----
>
> The comment that starts with:
> # We are using this on windows and darwin.
> # For windows the logic is as follow:
> # ....
>
> is still crap. The whole 9 lines of it, so it's not completely your fault, but
> it's crap.
> Let's rewrite it with something simpler like this:
>
> # We need to manually check if the path is a folder, because
> # neither ReadDirectoryChangesW nor the FSEvents API tell us
>
> Also the docstring in that function lies about "update the local subdir list".
>
> ---

The comment has been fixed, and the isinstance thing is fixed in the next branch which involves actually the darwin things as we talk.

Alejandro J. Cura (alecu) wrote :

Code looks fine. Ran tests on Precise and windows, and they all pass.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'tests/platform/filesystem_notifications/test_windows.py'
2--- tests/platform/filesystem_notifications/test_windows.py 2012-05-14 21:24:24 +0000
3+++ tests/platform/filesystem_notifications/test_windows.py 2012-06-26 19:23:19 +0000
4@@ -51,6 +51,7 @@
5 IN_OPEN,
6 )
7 from ubuntuone.platform.filesystem_notifications import (
8+ common,
9 windows as filesystem_notifications,
10 )
11 from ubuntuone.platform.filesystem_notifications.windows import (
12@@ -65,12 +66,11 @@
13 FILE_NOTIFY_CHANGE_LAST_WRITE,
14 FILE_NOTIFY_CHANGE_SECURITY,
15 FILE_NOTIFY_CHANGE_LAST_ACCESS,
16- WINDOWS_ACTIONS_NAMES,
17 )
18
19 #create a rever mapping to use it in the tests.
20 REVERSE_WINDOWS_ACTIONS = {}
21-for key, value in filesystem_notifications.WINDOWS_ACTIONS.iteritems():
22+for key, value in common.COMMON_ACTIONS.iteritems():
23 REVERSE_WINDOWS_ACTIONS[value] = key
24
25
26@@ -147,7 +147,7 @@
27 # group all events in a single lists which is not what the COM API
28 # does.
29 str_events = [
30- (WINDOWS_ACTIONS_NAMES[action], path) for action, path in
31+ (common.COMMON_ACTIONS_NAMES[action], path) for action, path in
32 events]
33 self.raw_events.append(str_events)
34 return events
35
36=== modified file 'ubuntuone/platform/filesystem_notifications/__init__.py'
37--- ubuntuone/platform/filesystem_notifications/__init__.py 2012-05-16 16:24:23 +0000
38+++ ubuntuone/platform/filesystem_notifications/__init__.py 2012-06-26 19:23:19 +0000
39@@ -32,6 +32,7 @@
40
41
42 if sys.platform == "win32":
43+ EVENT_CODES = [1, 2, 3, 4, 5]
44 from ubuntuone.platform.filesystem_notifications import windows
45 FilesystemMonitor = windows.FilesystemMonitor
46 _GeneralINotifyProcessor = windows.NotifyProcessor
47
48=== renamed file 'ubuntuone/platform/filesystem_notifications/windows.py' => 'ubuntuone/platform/filesystem_notifications/common.py'
49--- ubuntuone/platform/filesystem_notifications/windows.py 2012-05-14 21:24:24 +0000
50+++ ubuntuone/platform/filesystem_notifications/common.py 2012-06-26 19:23:19 +0000
51@@ -26,41 +26,14 @@
52 # do not wish to do so, delete this exception statement from your
53 # version. If you delete this exception statement from all source
54 # files in the program, then also delete it here.
55-"""File notifications on windows."""
56+"""Generic File notifications."""
57
58 import logging
59 import os
60
61-from uuid import uuid4
62+from twisted.internet import defer
63
64-from twisted.internet import defer, reactor
65-from twisted.python.failure import Failure
66-from pywintypes import OVERLAPPED
67-from win32api import CloseHandle
68-from win32con import (
69- FILE_SHARE_READ,
70- FILE_SHARE_WRITE,
71- FILE_FLAG_BACKUP_SEMANTICS,
72- FILE_NOTIFY_CHANGE_FILE_NAME,
73- FILE_NOTIFY_CHANGE_DIR_NAME,
74- FILE_NOTIFY_CHANGE_ATTRIBUTES,
75- FILE_NOTIFY_CHANGE_SIZE,
76- FILE_NOTIFY_CHANGE_LAST_WRITE,
77- FILE_NOTIFY_CHANGE_SECURITY,
78- OPEN_EXISTING)
79-from win32file import (
80- AllocateReadBuffer,
81- CreateFileW,
82- GetOverlappedResult,
83- ReadDirectoryChangesW,
84- FILE_FLAG_OVERLAPPED,
85- FILE_NOTIFY_INFORMATION)
86-from win32event import (
87- CreateEvent,
88- INFINITE,
89- SetEvent,
90- WaitForMultipleObjects,
91- WAIT_OBJECT_0)
92+from ubuntuone.platform.filesystem_notifications import EVENT_CODES
93 from ubuntuone.platform.filesystem_notifications.pyinotify_agnostic import (
94 Event,
95 WatchManagerError,
96@@ -75,40 +48,32 @@
97 IN_MOVED_FROM,
98 IN_MOVED_TO,
99 IN_MODIFY)
100+
101+from ubuntuone import logger
102+
103 from ubuntuone.platform.os_helper.windows import (
104 is_valid_syncdaemon_path,
105- is_valid_windows_path,
106- get_syncdaemon_valid_path,
107- windowspath,
108+ is_valid_windows_path as is_valid_os_path,
109+ windowspath as os_path,
110 )
111-from ubuntuone import logger
112-
113-# our logging level
114-TRACE = logger.TRACE
115-
116-# constant found in the msdn documentation:
117-# http://msdn.microsoft.com/en-us/library/ff538834(v=vs.85).aspx
118-FILE_LIST_DIRECTORY = 0x0001
119-FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
120-FILE_NOTIFY_CHANGE_CREATION = 0x00000040
121-
122-# a map between the few events that we have on windows and those
123+
124+# a map between the few events that we have on common platforms and those
125 # found in pyinotify
126-WINDOWS_ACTIONS = {
127- 1: IN_CREATE,
128- 2: IN_DELETE,
129- 3: IN_MODIFY,
130- 4: IN_MOVED_FROM,
131- 5: IN_MOVED_TO,
132+COMMON_ACTIONS = {
133+ EVENT_CODES[0]: IN_CREATE,
134+ EVENT_CODES[1]: IN_DELETE,
135+ EVENT_CODES[2]: IN_MODIFY,
136+ EVENT_CODES[3]: IN_MOVED_FROM,
137+ EVENT_CODES[4]: IN_MOVED_TO,
138 }
139
140 # a map of the actions to names so that we have better logs.
141-WINDOWS_ACTIONS_NAMES = {
142- 1: 'IN_CREATE',
143- 2: 'IN_DELETE',
144- 3: 'IN_MODIFY',
145- 4: 'IN_MOVED_FROM',
146- 5: 'IN_MOVED_TO',
147+COMMON_ACTIONS_NAMES = {
148+ EVENT_CODES[0]: 'IN_CREATE',
149+ EVENT_CODES[1]: 'IN_DELETE',
150+ EVENT_CODES[2]: 'IN_MODIFY',
151+ EVENT_CODES[3]: 'IN_MOVED_FROM',
152+ EVENT_CODES[4]: 'IN_MOVED_TO',
153 }
154
155 # translates quickly the event and it's is_dir state to our standard events
156@@ -125,17 +90,8 @@
157 IN_MOVED_TO: 'FS_FILE_CREATE',
158 IN_MOVED_TO | IN_ISDIR: 'FS_DIR_CREATE'}
159
160-# the default mask to be used in the watches added by the FilesystemMonitor
161-# class
162-FILESYSTEM_MONITOR_MASK = FILE_NOTIFY_CHANGE_FILE_NAME | \
163- FILE_NOTIFY_CHANGE_DIR_NAME | \
164- FILE_NOTIFY_CHANGE_ATTRIBUTES | \
165- FILE_NOTIFY_CHANGE_SIZE | \
166- FILE_NOTIFY_CHANGE_LAST_WRITE | \
167- FILE_NOTIFY_CHANGE_SECURITY | \
168- FILE_NOTIFY_CHANGE_LAST_ACCESS
169-
170-THREADPOOL_MAX = 20
171+# our logging level
172+TRACE = logger.TRACE
173
174
175 # The implementation of the code that is provided as the pyinotify substitute
176@@ -144,15 +100,11 @@
177
178 def __init__(self, watch_descriptor, path, mask, auto_add, processor,
179 buf_size=8192):
180- super(Watch, self).__init__()
181- self.log = logging.getLogger('ubuntuone.SyncDaemon.platform.windows.' +
182+ self.log = logging.getLogger('ubuntuone.SyncDaemon.platform.common.' +
183 'filesystem_notifications.Watch')
184 self.log.setLevel(TRACE)
185 self._processor = processor
186 self._buf_size = buf_size
187- self._wait_stop = CreateEvent(None, 0, 0, None)
188- self._overlapped = OVERLAPPED()
189- self._overlapped.hEvent = CreateEvent(None, 0, 0, None)
190 self._watching = False
191 self._descriptor = watch_descriptor
192 self._auto_add = auto_add
193@@ -170,36 +122,25 @@
194 path += os.path.sep
195 self._path = os.path.abspath(path)
196 self._mask = mask
197- # this deferred is fired when the watch has started monitoring
198- # a directory from a thread
199- self._watch_started_deferred = defer.Deferred()
200- # and this one is fired when the watch has stopped
201- self._watch_stopped_deferred = defer.Deferred()
202
203- @is_valid_windows_path(path_indexes=[1])
204+ @is_valid_os_path(path_indexes=[1])
205 def _update_subdirs(self, path, event):
206 """Adds the path to the internal subdirs.
207
208 The given path is considered to be a path and therefore this
209 will not be checked.
210 """
211- if WINDOWS_ACTIONS[event] == IN_CREATE:
212+ if COMMON_ACTIONS[event] == IN_CREATE:
213 self._subdirs.add(path)
214- elif WINDOWS_ACTIONS[event] == IN_DELETE and\
215- path in self._subdirs:
216+ elif COMMON_ACTIONS[event] == IN_DELETE and path in self._subdirs:
217 self._subdirs.remove(path)
218
219- @is_valid_windows_path(path_indexes=[1])
220+ @is_valid_os_path(path_indexes=[1])
221 def _path_is_dir(self, path):
222- """Check if the path is a dir and update the local subdir list."""
223+ """Check if the path is a dir."""
224
225- # The logic of this function is the following:
226- # 1. ReadDirectoryChangesW changes does not send if a path
227- # is a new dir or not.
228- # 2. We keep track of subdirs that in self._subdir.
229- # 3. We check if a path is a dir by:
230- # * Asking the os if the path exists.
231- # * Finding the path in self._subdirs
232+ # We need to manually check if the path is a folder, because
233+ # neither ReadDirectoryChangesW nor the FSEvents API tell us
234
235 is_dir = False
236 if os.path.exists(path):
237@@ -210,131 +151,7 @@
238 self.log.debug('Is path %r a dir? %s', path, is_dir)
239 return is_dir
240
241- def _process_events(self, events):
242- """Process the events from the queue."""
243- # do not do it if we stop watching and the events are empty
244- if not self._watching:
245- return
246-
247- # we transform the events to be the same as the one in pyinotify
248- # and then use the proc_fun
249- for action, file_name in events:
250- if any([file_name.startswith(path)
251- for path in self._ignore_paths]):
252- continue
253- # map the windows events to the pyinotify ones, tis is dirty but
254- # makes the multiplatform better, linux was first :P
255- syncdaemon_path = get_syncdaemon_valid_path(
256- os.path.join(self._path, file_name))
257- full_dir_path = os.path.join(self._path, file_name)
258- is_dir = self._path_is_dir(full_dir_path)
259- if is_dir:
260- # we need to update the list of subdirs that we have
261- self._update_subdirs(full_dir_path, action)
262- mask = WINDOWS_ACTIONS[action]
263- head, tail = os.path.split(file_name)
264- if is_dir:
265- mask |= IN_ISDIR
266- event_raw_data = {
267- 'wd': self._descriptor,
268- 'dir': is_dir,
269- 'mask': mask,
270- 'name': tail,
271- 'path': '.'}
272- # by the way in which the win api fires the events we know for
273- # sure that no move events will be added in the wrong order, this
274- # is kind of hacky, I dont like it too much
275- if WINDOWS_ACTIONS[action] == IN_MOVED_FROM:
276- self._cookie = str(uuid4())
277- self._source_pathname = tail
278- event_raw_data['cookie'] = self._cookie
279- if WINDOWS_ACTIONS[action] == IN_MOVED_TO:
280- event_raw_data['src_pathname'] = self._source_pathname
281- event_raw_data['cookie'] = self._cookie
282- event = Event(event_raw_data)
283- # FIXME: event deduces the pathname wrong and we need to manually
284- # set it
285- event.pathname = syncdaemon_path
286- # add the event only if we do not have an exclude filter or
287- # the exclude filter returns False, that is, the event will not
288- # be excluded
289- self.log.debug('Pushing event %r to processor.', event)
290- self._processor(event)
291-
292- def _call_deferred(self, f, *args):
293- """Executes the deferred call avoiding possible race conditions."""
294- if not self._watch_started_deferred.called:
295- f(*args)
296-
297- def _watch_wrapper(self):
298- """Wrap _watch, and errback on any unhandled error."""
299- try:
300- self._watch()
301- except Exception:
302- reactor.callFromThread(self._call_deferred,
303- self._watch_started_deferred.errback, Failure())
304-
305- def _watch(self):
306- """Watch a path that is a directory."""
307- self.log.debug('Adding watch for %r (exists? %r is dir? %r).',
308- self._path,
309- os.path.exists(self._path), os.path.isdir(self._path))
310- # we are going to be using the ReadDirectoryChangesW whihc requires
311- # a directory handle and the mask to be used.
312- self._watch_handle = CreateFileW(
313- self._path,
314- FILE_LIST_DIRECTORY,
315- FILE_SHARE_READ | FILE_SHARE_WRITE,
316- None,
317- OPEN_EXISTING,
318- FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
319- None)
320-
321- try:
322- self._watch_loop(self._watch_handle)
323- finally:
324- CloseHandle(self._watch_handle)
325- self._watch_handle = None
326- reactor.callFromThread(self.stopped.callback, True)
327-
328- def _watch_loop(self, handle):
329- """The loop where we watch the directory."""
330- while True:
331- # important information to know about the parameters:
332- # param 1: the handle to the dir
333- # param 2: the size to be used in the kernel to store events
334- # that might be lost while the call is being performed. This
335- # is complicated to fine tune since if you make lots of watcher
336- # you migh used too much memory and make your OS to BSOD
337- buf = AllocateReadBuffer(self._buf_size)
338- ReadDirectoryChangesW(
339- handle,
340- buf,
341- self._auto_add,
342- self._mask,
343- self._overlapped,
344- )
345- if not self._watch_started_deferred.called:
346- reactor.callFromThread(self._call_deferred,
347- self._watch_started_deferred.callback, True)
348- # wait for an event and ensure that we either stop or read the
349- # data
350- rc = WaitForMultipleObjects((self._wait_stop,
351- self._overlapped.hEvent),
352- 0, INFINITE)
353- if rc == WAIT_OBJECT_0:
354- # Stop event
355- break
356- # if we continue, it means that we got some data, lets read it
357- data = GetOverlappedResult(handle, self._overlapped, True)
358- # lets ead the data and store it in the results
359- events = FILE_NOTIFY_INFORMATION(buf, data)
360- self.log.debug('Got from ReadDirectoryChangesW %r.',
361- [(WINDOWS_ACTIONS_NAMES[action], path) for action, path in
362- events])
363- reactor.callFromThread(self._process_events, events)
364-
365- @is_valid_windows_path(path_indexes=[1])
366+ @is_valid_os_path(path_indexes=[1])
367 def ignore_path(self, path):
368 """Add the path of the events to ignore."""
369 if not path.endswith(os.path.sep):
370@@ -343,7 +160,7 @@
371 path = path[len(self._path):]
372 self._ignore_paths.append(path)
373
374- @is_valid_windows_path(path_indexes=[1])
375+ @is_valid_os_path(path_indexes=[1])
376 def remove_ignored_path(self, path):
377 """Reaccept path."""
378 if not path.endswith(os.path.sep):
379@@ -363,16 +180,12 @@
380 # process the events.
381 self.log.debug('Start watching path.')
382 self._watching = True
383- reactor.callInThread(self._watch_wrapper)
384- return self._watch_started_deferred
385
386 def stop_watching(self):
387 """Tell the watch to stop processing events."""
388 self.log.info('Stop watching %s', self._path)
389- SetEvent(self._wait_stop)
390 self._watching = False
391 self._subdirs = set()
392- return self.stopped
393
394 def update(self, mask, auto_add=False):
395 """Update the info used by the watcher."""
396@@ -389,29 +202,18 @@
397 def auto_add(self):
398 return self._auto_add
399
400- @property
401- def started(self):
402- """A deferred that will be called when the watch is running."""
403- return self._watch_started_deferred
404-
405- @property
406- def stopped(self):
407- """A deferred fired when the watch thread has finished."""
408- return self._watch_stopped_deferred
409-
410
411 class WatchManager(object):
412 """Implement the same functions as pyinotify.WatchManager.
413
414- All paths passed to methods in this class should be windows paths.
415+ All paths passed to methods in this class should be proper os paths.
416
417 """
418
419 def __init__(self, processor):
420 """Init the manager to keep trak of the different watches."""
421- super(WatchManager, self).__init__()
422 self._processor = processor
423- self.log = logging.getLogger('ubuntuone.SyncDaemon.platform.windows.'
424+ self.log = logging.getLogger('ubuntuone.SyncDaemon.platform.common.'
425 + 'filesystem_notifications.WatchManager')
426 self.log.setLevel(TRACE)
427 self._wdm = {}
428@@ -420,12 +222,8 @@
429
430 def stop(self):
431 """Close the manager and stop all watches."""
432- self.log.debug('Stopping watches.')
433- wait_list = []
434- for current_wd in self._wdm:
435- wait_list.append(self._wdm[current_wd].stop_watching())
436- self.log.debug('Stopping Watch on %r.', self._wdm[current_wd].path)
437- return defer.DeferredList(wait_list)
438+ # Should be implemented for each platform
439+ raise NotImplementedError("Not implemented on this platform.")
440
441 def get_watch(self, wd):
442 """Return the watch with the given descriptor."""
443@@ -442,8 +240,7 @@
444 except KeyError, e:
445 logging.error(str(e))
446
447- def _add_single_watch(self, path, mask, auto_add=False,
448- quiet=True):
449+ def _add_single_watch(self, path, mask, auto_add=False, quiet=True):
450 if path in self._ignored_paths:
451 # simply removed it from the filter
452 self._ignored_paths.remove(path)
453@@ -452,23 +249,21 @@
454 self.log.debug('add_single_watch(%s, %s, %s, %s)', path, mask,
455 auto_add, quiet)
456
457- # adjust the number of threads based on the UDFs watched
458- reactor.suggestThreadPoolSize(THREADPOOL_MAX + self._wd_count + 1)
459- self._wdm[self._wd_count] = Watch(self._wd_count, path,
460- mask, auto_add, self._processor)
461- d = self._wdm[self._wd_count].start_watching()
462- self._wd_count += 1
463- return d
464-
465- @is_valid_windows_path(path_indexes=[1])
466- def add_watch(self, path, mask, auto_add=False,
467- quiet=True):
468+ return self._adding_watch(path, mask, auto_add)
469+
470+ def _adding_watch(self, path, mask, auto_add):
471+ """Add the watch to the dict and start it."""
472+ # This should be implemented for each OS
473+ raise NotImplementedError("Not implemented on this platform.")
474+
475+ @is_valid_os_path(path_indexes=[1])
476+ def add_watch(self, path, mask, auto_add=False, quiet=True):
477 """Add a new path tp be watch.
478
479 The method will ensure that the path is not already present.
480 """
481 if not isinstance(path, unicode):
482- e = NotImplementedError("No implementation on windows.")
483+ e = NotImplementedError("No implementation on this platform.")
484 return defer.fail(e)
485 wd = self.get_wd(path)
486 if wd is None:
487@@ -480,9 +275,9 @@
488
489 def update_watch(self, wd, mask=None, rec=False,
490 auto_add=False, quiet=True):
491- raise NotImplementedError("Not implemented on windows.")
492+ raise NotImplementedError("Not implemented on this platform.")
493
494- @is_valid_windows_path(path_indexes=[1])
495+ @is_valid_os_path(path_indexes=[1])
496 def get_wd(self, path):
497 """Return the watcher that is used to watch the given path."""
498 if not path.endswith(os.path.sep):
499@@ -512,7 +307,7 @@
500 if not quiet:
501 raise WatchManagerError('Watch %s was not found' % wd, {})
502
503- @is_valid_windows_path(path_indexes=[1])
504+ @is_valid_os_path(path_indexes=[1])
505 def rm_path(self, path):
506 """Remove a watch to the given path."""
507 wd = self.get_wd(path)
508@@ -527,9 +322,6 @@
509 This interface will be exposed to syncdaemon, ergo all passed
510 and returned paths must be a sequence of BYTES encoded with utf8.
511
512- Also, they must not be literal paths, that is the \\?\ prefix should not be
513- in the path.
514-
515 """
516
517 def __init__(self, monitor, ignore_config=None):
518@@ -549,13 +341,10 @@
519 """Add an event and path(s) to the mute filter."""
520 self.general_processor.add_to_mute_filter(event, paths)
521
522- @is_valid_syncdaemon_path(path_indexes=[1])
523 def platform_is_ignored(self, path):
524 """Should we ignore this path in the current platform.?"""
525- # don't support links yet
526- if path.endswith('.lnk'):
527- return True
528- return False
529+ # This should be implemented in each platform
530+ raise NotImplementedError("Not implemented on this platform.")
531
532 @is_valid_syncdaemon_path(path_indexes=[1])
533 def is_ignored(self, path):
534@@ -572,7 +361,7 @@
535 # lets ignore dir changes
536 if event.dir:
537 return
538- # on windows we just get IN_MODIFY, lets always fake
539+ # on someplatforms we just get IN_MODIFY, lets always fake
540 # an OPEN & CLOSE_WRITE couple
541 raw_open = raw_close = {
542 'wd': event.wd,
543@@ -665,7 +454,7 @@
544 self.release_held_event()
545 self.general_processor.push_event(event)
546 else:
547- # We should never get here on windows, I really do not know how we
548+ # We should never get here, I really do not know how we
549 # got here
550 self.general_processor.log.warn(
551 'Cookie does not match the previoues held event!')
552@@ -739,11 +528,12 @@
553
554 def __init__(self, eq, fs, ignore_config=None, timeout=1):
555 self.log = logging.getLogger('ubuntuone.SyncDaemon.FSMonitor')
556+ self.filesystem_monitor_mask = None
557 self.log.setLevel(TRACE)
558 self.fs = fs
559 self.eq = eq
560- self._processor = NotifyProcessor(self, ignore_config)
561- self._watch_manager = WatchManager(self._processor)
562+ # You will need to create the NotifyProcessor and WatchManager
563+ # in each OS-specific implementation
564
565 def add_to_mute_filter(self, event, **info):
566 """Add info to mute filter in the processor."""
567@@ -757,27 +547,24 @@
568 """Prepares the EQ to be closed."""
569 return self._watch_manager.stop()
570
571- @windowspath(path_indexes=[1])
572+ @os_path(path_indexes=[1])
573 def rm_watch(self, dirpath):
574 """Remove watch from a dir."""
575 # trust the implementation of the manager
576 self._watch_manager.rm_path(dirpath)
577
578- @windowspath(path_indexes=[1])
579+ @os_path(path_indexes=[1])
580 def add_watch(self, dirpath):
581 """Add watch to a dir."""
582 # the logic to check if the watch is already set
583 # is all in WatchManager.add_watch
584 return self._watch_manager.add_watch(dirpath,
585- FILESYSTEM_MONITOR_MASK, auto_add=True)
586+ self.filesystem_monitor_mask, auto_add=True)
587
588 def add_watches_to_udf_ancestors(self, volume):
589 """Add a inotify watch to volume's ancestors if it's an UDF."""
590- # On windows there is no need to add the watches to the ancestors
591- # so we will always return true. The reason for this is that the
592- # handles that we open stop the user from renaming the ancestors of
593- # the UDF, for a user to do that he has to unsync the udf first
594- return defer.succeed(True)
595+
596+ # Should be implemented in each OS if necessary
597
598 def is_frozen(self):
599 """Checks if there's something frozen."""
600
601=== added file 'ubuntuone/platform/filesystem_notifications/windows.py'
602--- ubuntuone/platform/filesystem_notifications/windows.py 1970-01-01 00:00:00 +0000
603+++ ubuntuone/platform/filesystem_notifications/windows.py 2012-06-26 19:23:19 +0000
604@@ -0,0 +1,308 @@
605+# -*- coding: utf-8 *-*
606+#
607+# Copyright 2011-2012 Canonical Ltd.
608+#
609+# This program is free software: you can redistribute it and/or modify it
610+# under the terms of the GNU General Public License version 3, as published
611+# by the Free Software Foundation.
612+#
613+# This program is distributed in the hope that it will be useful, but
614+# WITHOUT ANY WARRANTY; without even the implied warranties of
615+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
616+# PURPOSE. See the GNU General Public License for more details.
617+#
618+# You should have received a copy of the GNU General Public License along
619+# with this program. If not, see <http://www.gnu.org/licenses/>.
620+#
621+# In addition, as a special exception, the copyright holders give
622+# permission to link the code of portions of this program with the
623+# OpenSSL library under certain conditions as described in each
624+# individual source file, and distribute linked combinations
625+# including the two.
626+# You must obey the GNU General Public License in all respects
627+# for all of the code used other than OpenSSL. If you modify
628+# file(s) with this exception, you may extend this exception to your
629+# version of the file(s), but you are not obligated to do so. If you
630+# do not wish to do so, delete this exception statement from your
631+# version. If you delete this exception statement from all source
632+# files in the program, then also delete it here.
633+"""File notifications on windows."""
634+
635+import os
636+from uuid import uuid4
637+
638+from twisted.internet import defer, reactor
639+from twisted.python.failure import Failure
640+
641+from pywintypes import OVERLAPPED
642+from win32api import CloseHandle
643+from win32con import (
644+ FILE_SHARE_READ,
645+ FILE_SHARE_WRITE,
646+ FILE_FLAG_BACKUP_SEMANTICS,
647+ FILE_NOTIFY_CHANGE_FILE_NAME,
648+ FILE_NOTIFY_CHANGE_DIR_NAME,
649+ FILE_NOTIFY_CHANGE_ATTRIBUTES,
650+ FILE_NOTIFY_CHANGE_SIZE,
651+ FILE_NOTIFY_CHANGE_LAST_WRITE,
652+ FILE_NOTIFY_CHANGE_SECURITY,
653+ OPEN_EXISTING)
654+from win32file import (
655+ AllocateReadBuffer,
656+ CreateFileW,
657+ GetOverlappedResult,
658+ ReadDirectoryChangesW,
659+ FILE_FLAG_OVERLAPPED,
660+ FILE_NOTIFY_INFORMATION)
661+from win32event import (
662+ CreateEvent,
663+ INFINITE,
664+ SetEvent,
665+ WaitForMultipleObjects,
666+ WAIT_OBJECT_0)
667+
668+from ubuntuone.platform.os_helper.windows import (
669+ is_valid_syncdaemon_path,
670+ get_syncdaemon_valid_path,
671+)
672+
673+from ubuntuone.platform.filesystem_notifications import common
674+
675+
676+# constant found in the msdn documentation:
677+# http://msdn.microsoft.com/en-us/library/ff538834(v=vs.85).aspx
678+FILE_LIST_DIRECTORY = 0x0001
679+FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
680+FILE_NOTIFY_CHANGE_CREATION = 0x00000040
681+
682+THREADPOOL_MAX = 20
683+
684+
685+class Watch(common.Watch):
686+ """Implement the same functions as pyinotify.Watch."""
687+
688+ def __init__(self, watch_descriptor, path, mask, auto_add, processor,
689+ buf_size=8192):
690+ super(Watch, self).__init__(watch_descriptor, path, mask, auto_add,
691+ processor, buf_size)
692+ self._wait_stop = CreateEvent(None, 0, 0, None)
693+ self._overlapped = OVERLAPPED()
694+ self._overlapped.hEvent = CreateEvent(None, 0, 0, None)
695+ # remember the subdirs we have so that when we have a delete we can
696+ # check if it was a remove
697+ self._subdirs = set()
698+ # this deferred is fired when the watch has started monitoring
699+ # a directory from a thread
700+ self._watch_started_deferred = defer.Deferred()
701+ # and this one is fired when the watch has stopped
702+ self._watch_stopped_deferred = defer.Deferred()
703+
704+ def _process_events(self, events):
705+ """Process the events from the queue."""
706+ # do not do it if we stop watching and the events are empty
707+ if not self._watching:
708+ return
709+
710+ # we transform the events to be the same as the one in pyinotify
711+ # and then use the proc_fun
712+ for action, file_name in events:
713+ if any([file_name.startswith(path)
714+ for path in self._ignore_paths]):
715+ continue
716+ # map the windows events to the pyinotify ones, tis is dirty but
717+ # makes the multiplatform better, linux was first :P
718+ syncdaemon_path = get_syncdaemon_valid_path(
719+ os.path.join(self._path, file_name))
720+ full_dir_path = os.path.join(self._path, file_name)
721+ is_dir = self._path_is_dir(full_dir_path)
722+ if is_dir:
723+ # we need to update the list of subdirs that we have
724+ self._update_subdirs(full_dir_path, action)
725+ mask = common.COMMON_ACTIONS[action]
726+ head, tail = os.path.split(file_name)
727+ if is_dir:
728+ mask |= common.IN_ISDIR
729+ event_raw_data = {
730+ 'wd': self._descriptor,
731+ 'dir': is_dir,
732+ 'mask': mask,
733+ 'name': tail,
734+ 'path': '.'}
735+ # by the way in which the win api fires the events we know for
736+ # sure that no move events will be added in the wrong order, this
737+ # is kind of hacky, I dont like it too much
738+ if common.COMMON_ACTIONS[action] == common.IN_MOVED_FROM:
739+ self._cookie = str(uuid4())
740+ self._source_pathname = tail
741+ event_raw_data['cookie'] = self._cookie
742+ if common.COMMON_ACTIONS[action] == common.IN_MOVED_TO:
743+ event_raw_data['src_pathname'] = self._source_pathname
744+ event_raw_data['cookie'] = self._cookie
745+ event = common.Event(event_raw_data)
746+ # FIXME: event deduces the pathname wrong and we need to manually
747+ # set it
748+ event.pathname = syncdaemon_path
749+ # add the event only if we do not have an exclude filter or
750+ # the exclude filter returns False, that is, the event will not
751+ # be excluded
752+ self.log.debug('Pushing event %r to processor.', event)
753+ self._processor(event)
754+
755+ def _call_deferred(self, f, *args):
756+ """Executes the deferred call avoiding possible race conditions."""
757+ if not self._watch_started_deferred.called:
758+ f(*args)
759+
760+ def _watch_wrapper(self):
761+ """Wrap _watch, and errback on any unhandled error."""
762+ try:
763+ self._watch()
764+ except Exception:
765+ reactor.callFromThread(self._call_deferred,
766+ self._watch_started_deferred.errback, Failure())
767+
768+ def _watch(self):
769+ """Watch a path that is a directory."""
770+ self.log.debug('Adding watch for %r (exists? %r is dir? %r).',
771+ self._path,
772+ os.path.exists(self._path), os.path.isdir(self._path))
773+ # we are going to be using the ReadDirectoryChangesW whihc requires
774+ # a directory handle and the mask to be used.
775+ self._watch_handle = CreateFileW(
776+ self._path,
777+ FILE_LIST_DIRECTORY,
778+ FILE_SHARE_READ | FILE_SHARE_WRITE,
779+ None,
780+ OPEN_EXISTING,
781+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
782+ None)
783+
784+ try:
785+ self._watch_loop(self._watch_handle)
786+ finally:
787+ CloseHandle(self._watch_handle)
788+ self._watch_handle = None
789+ reactor.callFromThread(self.stopped.callback, True)
790+
791+ def _watch_loop(self, handle):
792+ """The loop where we watch the directory."""
793+ while True:
794+ # important information to know about the parameters:
795+ # param 1: the handle to the dir
796+ # param 2: the size to be used in the kernel to store events
797+ # that might be lost while the call is being performed. This
798+ # is complicated to fine tune since if you make lots of watcher
799+ # you migh used too much memory and make your OS to BSOD
800+ buf = AllocateReadBuffer(self._buf_size)
801+ ReadDirectoryChangesW(
802+ handle,
803+ buf,
804+ self._auto_add,
805+ self._mask,
806+ self._overlapped,
807+ )
808+ if not self._watch_started_deferred.called:
809+ reactor.callFromThread(self._call_deferred,
810+ self._watch_started_deferred.callback, True)
811+ # wait for an event and ensure that we either stop or read the
812+ # data
813+ rc = WaitForMultipleObjects((self._wait_stop,
814+ self._overlapped.hEvent),
815+ 0, INFINITE)
816+ if rc == WAIT_OBJECT_0:
817+ # Stop event
818+ break
819+ # if we continue, it means that we got some data, lets read it
820+ data = GetOverlappedResult(handle, self._overlapped, True)
821+ # lets ead the data and store it in the results
822+ events = FILE_NOTIFY_INFORMATION(buf, data)
823+ self.log.debug('Got from ReadDirectoryChangesW %r.',
824+ [(common.COMMON_ACTIONS_NAMES[action], path) \
825+ for action, path in events])
826+ reactor.callFromThread(self._process_events, events)
827+
828+ def start_watching(self):
829+ """Tell the watch to start processing events."""
830+ super(Watch, self).start_watching()
831+ reactor.callInThread(self._watch_wrapper)
832+ return self._watch_started_deferred
833+
834+ def stop_watching(self):
835+ """Tell the watch to stop processing events."""
836+ super(Watch, self).stop_watching()
837+ SetEvent(self._wait_stop)
838+ return self.stopped
839+
840+ @property
841+ def started(self):
842+ """A deferred that will be called when the watch is running."""
843+ return self._watch_started_deferred
844+
845+ @property
846+ def stopped(self):
847+ """A deferred fired when the watch thread has finished."""
848+ return self._watch_stopped_deferred
849+
850+
851+class WatchManager(common.WatchManager):
852+
853+ def stop(self):
854+ """Close the manager and stop all watches."""
855+ self.log.debug('Stopping watches.')
856+ wait_list = []
857+ for current_wd in self._wdm:
858+ wait_list.append(self._wdm[current_wd].stop_watching())
859+ self.log.debug('Stopping Watch on %r.', self._wdm[current_wd].path)
860+ return defer.DeferredList(wait_list)
861+
862+ def _adding_watch(self, path, mask, auto_add):
863+ # adjust the number of threads based on the UDFs watched
864+ watch = Watch(self._wd_count, path, mask, auto_add, self._processor)
865+ reactor.suggestThreadPoolSize(THREADPOOL_MAX + self._wd_count + 1)
866+ self._wdm[self._wd_count] = watch
867+ d = self._wdm[self._wd_count].start_watching()
868+ self._wd_count += 1
869+ return d
870+
871+
872+class NotifyProcessor(common.NotifyProcessor):
873+ """
874+ Processor that takes care of dealing with the events.
875+
876+ Also, they must not be literal paths, that is the \\?\ prefix should not be
877+ in the path.
878+ """
879+
880+ @is_valid_syncdaemon_path(path_indexes=[1])
881+ def platform_is_ignored(self, path):
882+ """Should we ignore this path in the current platform.?"""
883+ # don't support links yet
884+ if path.endswith('.lnk'):
885+ return True
886+ return False
887+
888+
889+class FilesystemMonitor(common.FilesystemMonitor):
890+ """Manages the signals from filesystem."""
891+
892+ def __init__(self, eq, fs, ignore_config=None, timeout=1):
893+ super(FilesystemMonitor, self).__init__(eq, fs, ignore_config, timeout)
894+ self._processor = NotifyProcessor(self, ignore_config)
895+ self._watch_manager = WatchManager(self._processor)
896+ # the default mask to be used in the watches
897+ # added by the FilesystemMonitor class
898+ self.filesystem_monitor_mask = FILE_NOTIFY_CHANGE_FILE_NAME | \
899+ FILE_NOTIFY_CHANGE_DIR_NAME | \
900+ FILE_NOTIFY_CHANGE_ATTRIBUTES | \
901+ FILE_NOTIFY_CHANGE_SIZE | \
902+ FILE_NOTIFY_CHANGE_LAST_WRITE | \
903+ FILE_NOTIFY_CHANGE_SECURITY | \
904+ FILE_NOTIFY_CHANGE_LAST_ACCESS
905+
906+ def add_watches_to_udf_ancestors(self, volume):
907+ """Add a inotify watch to volume's ancestors if it's an UDF."""
908+ # There is no need to add the watches to the ancestors
909+ # so we will always return true. The reason for this is that the
910+ # handles that we open stop the user from renaming the ancestors of
911+ # the UDF, for a user to do that he has to unsync the udf first
912+ return defer.succeed(True)

Subscribers

People subscribed via source and target branches