Merge lp:~diegosarmentero/ubuntuone-client/darwin2-fsevents into lp:ubuntuone-client
- darwin2-fsevents
- Merge into trunk
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 | ||||
Related bugs: |
|
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).
Description of the change
- 1263. By Diego Sarmentero
-
Updating branch
Diego Sarmentero (diegosarmentero) wrote : | # |
> The implementation in add_watches_
> 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_
> filesystem_
>
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_
> 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_
> > filesystem_
> >
>
> 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.
- 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
Diego Sarmentero (diegosarmentero) wrote : | # |
Branch fixed.
I added the event codes in the filesystem_
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.
Manuel de la Peña (mandel) : | # |
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 ReadDirectoryCh
Also the docstring in that function lies about "update the local subdir list".
---
- 1268. By Diego Sarmentero
-
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 ReadDirectoryCh
>
> 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.
Preview Diff
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) |
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: valid_syncdaemo n_path = None valid_os_ path = None
is_
is_
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.