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

Proposed by Diego Sarmentero
Status: Merged
Approved by: Alejandro J. Cura
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) Approve
Manuel de la Peña (community) Approve
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.
Revision history for this message
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

Updating branch

Revision history for this message
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

Revision history for this message
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

Revision history for this message
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

merge

1265. By Diego Sarmentero

updates

1266. By Diego Sarmentero

adding not implemented error

1267. By Diego Sarmentero

moving event codes to filesystem notification init

Revision history for this message
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.

Revision history for this message
Manuel de la Peña (mandel) :
review: Approve
Revision history for this message
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

fixing comment

Revision history for this message
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.

Revision history for this message
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