Merge lp:~mandel/ubuntuone-client/fsevents-daemon into lp:ubuntuone-client

Proposed by Manuel de la Peña on 2012-07-13
Status: Merged
Approved by: dobey on 2012-07-19
Approved revision: 1290
Merged at revision: 1277
Proposed branch: lp:~mandel/ubuntuone-client/fsevents-daemon
Merge into: lp:ubuntuone-client
Diff against target: 1413 lines (+1204/-135)
8 files modified
Makefile.am (+1/-1)
run-tests.bat (+1/-1)
tests/platform/filesystem_notifications/test_fsevents_daemon.py (+437/-0)
ubuntuone/platform/filesystem_notifications/common.py (+6/-4)
ubuntuone/platform/filesystem_notifications/darwin/__init__.py (+5/-129)
ubuntuone/platform/filesystem_notifications/darwin/fsevents_client.py (+159/-0)
ubuntuone/platform/filesystem_notifications/darwin/fsevents_daemon.py (+594/-0)
ubuntuone/syncdaemon/filesystem_manager.py (+1/-0)
To merge this branch: bzr merge lp:~mandel/ubuntuone-client/fsevents-daemon
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve on 2012-07-18
Diego Sarmentero (community) 2012-07-13 Approve on 2012-07-13
Review via email: mp+114836@code.launchpad.net

Commit Message

Added initial inclusion of the file system monitor that will allow to use the fsevents daemon in darwin.

Description of the Change

Added initial inclusion of the file system monitor that will allow to use the fsevents daemon in darwin.

To post a comment you must log in.
Diego Sarmentero (diegosarmentero) wrote :

+1

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

A few typos that I told mandel about; other than that, this is an awesome branch. Congrats mandel!

review: Approve
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (281.3 KiB)

The attempt to merge lp:~mandel/ubuntuone-client/fsevents-daemon into lp:ubuntuone-client failed. Below is the output from the failed tests.

/usr/bin/gnome-autogen.sh
checking for autoconf >= 2.53...
  testing autoconf2.50... not found.
  testing autoconf... found 2.69
checking for automake >= 1.10...
  testing automake-1.11... found 1.11.5
checking for libtool >= 1.5...
  testing libtoolize... found 2.4.2
checking for intltool >= 0.30...
  testing intltoolize... found 0.50.2
checking for pkg-config >= 0.14.0...
  testing pkg-config... found 0.26
checking for gtk-doc >= 1.0...
  testing gtkdocize... found 1.18
Checking for required M4 macros...
Checking for forbidden M4 macros...
Processing ./configure.ac
Running libtoolize...
libtoolize: putting auxiliary files in `.'.
libtoolize: copying file `./ltmain.sh'
libtoolize: putting macros in AC_CONFIG_MACRO_DIR, `m4'.
libtoolize: copying file `m4/libtool.m4'
libtoolize: copying file `m4/ltoptions.m4'
libtoolize: copying file `m4/ltsugar.m4'
libtoolize: copying file `m4/ltversion.m4'
libtoolize: copying file `m4/lt~obsolete.m4'
Running intltoolize...
Running gtkdocize...
Running aclocal-1.11...
Running autoconf...
Running autoheader...
Running automake-1.11...
Running ./configure --enable-gtk-doc --enable-debug ...
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... no
checking for mawk... mawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for style of include used by make... GNU
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking dependency style of gcc... gcc3
checking for library containing strerror... none required
checking for gcc... (cached) gcc
checking whether we are using the GNU C compiler... (cached) yes
checking whether gcc accepts -g... (cached) yes
checking for gcc option to accept ISO C89... (cached) none needed
checking dependency style of gcc... (cached) gcc3
checking build system type... x86_64-unknown-linux-gnu
checking host system type... x86_64-unknown-linux-gnu
checking how to print strings... printf
checking for a sed that does not truncate output... /bin/sed
checking for grep that handles long lines and -e... /bin/grep
checking for egrep... /bin/grep -E
checking for fgrep... /bin/grep -F
checking for ld used by gcc... /usr/bin/ld
checking if the linker (/usr/bin/ld) is GNU ld... yes
checking for BSD- or MS-compatible name lister (nm)... /usr/bin/nm -B
checking the name lister (/usr/bin/nm -B) interface... BSD nm
checking whether ln -s works... yes
checking the maximum length of command line arguments... 1572864
checking whether the shell understands some XSI constructs... yes
checking whether the shell understands "+="... yes
ch...

Manuel de la Peña (mandel) wrote :

I'll fix the typos etc.. before it lands :)

Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (281.3 KiB)

The attempt to merge lp:~mandel/ubuntuone-client/fsevents-daemon into lp:ubuntuone-client failed. Below is the output from the failed tests.

/usr/bin/gnome-autogen.sh
checking for autoconf >= 2.53...
  testing autoconf2.50... not found.
  testing autoconf... found 2.69
checking for automake >= 1.10...
  testing automake-1.11... found 1.11.5
checking for libtool >= 1.5...
  testing libtoolize... found 2.4.2
checking for intltool >= 0.30...
  testing intltoolize... found 0.50.2
checking for pkg-config >= 0.14.0...
  testing pkg-config... found 0.26
checking for gtk-doc >= 1.0...
  testing gtkdocize... found 1.18
Checking for required M4 macros...
Checking for forbidden M4 macros...
Processing ./configure.ac
Running libtoolize...
libtoolize: putting auxiliary files in `.'.
libtoolize: copying file `./ltmain.sh'
libtoolize: putting macros in AC_CONFIG_MACRO_DIR, `m4'.
libtoolize: copying file `m4/libtool.m4'
libtoolize: copying file `m4/ltoptions.m4'
libtoolize: copying file `m4/ltsugar.m4'
libtoolize: copying file `m4/ltversion.m4'
libtoolize: copying file `m4/lt~obsolete.m4'
Running intltoolize...
Running gtkdocize...
Running aclocal-1.11...
Running autoconf...
Running autoheader...
Running automake-1.11...
Running ./configure --enable-gtk-doc --enable-debug ...
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... no
checking for mawk... mawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for style of include used by make... GNU
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking dependency style of gcc... gcc3
checking for library containing strerror... none required
checking for gcc... (cached) gcc
checking whether we are using the GNU C compiler... (cached) yes
checking whether gcc accepts -g... (cached) yes
checking for gcc option to accept ISO C89... (cached) none needed
checking dependency style of gcc... (cached) gcc3
checking build system type... x86_64-unknown-linux-gnu
checking host system type... x86_64-unknown-linux-gnu
checking how to print strings... printf
checking for a sed that does not truncate output... /bin/sed
checking for grep that handles long lines and -e... /bin/grep
checking for egrep... /bin/grep -E
checking for fgrep... /bin/grep -F
checking for ld used by gcc... /usr/bin/ld
checking if the linker (/usr/bin/ld) is GNU ld... yes
checking for BSD- or MS-compatible name lister (nm)... /usr/bin/nm -B
checking the name lister (/usr/bin/nm -B) interface... BSD nm
checking whether ln -s works... yes
checking the maximum length of command line arguments... 1572864
checking whether the shell understands some XSI constructs... yes
checking whether the shell understands "+="... yes
ch...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile.am'
2--- Makefile.am 2012-05-15 13:25:37 +0000
3+++ Makefile.am 2012-07-19 14:21:30 +0000
4@@ -54,7 +54,7 @@
5 test: logging.conf $(clientdefs_DATA) Makefile
6 echo "$(PYTHONPATH)"
7 if test "x$(builddir)" == "x$(srcdir)"; then \
8- PYTHONPATH="$(PYTHONPATH)" u1trial -r $(REACTOR) -p tests/platform/windows,tests/proxy -i "test_windows.py,test_darwin.py" tests || exit 1; \
9+ PYTHONPATH="$(PYTHONPATH)" u1trial -r $(REACTOR) -p tests/platform/windows,tests/proxy -i "test_windows.py,test_darwin.py,test_fsevents_daemon.py" tests || exit 1; \
10 PYTHONPATH="$(PYTHONPATH)" u1trial -r qt4 -p tests/platform/windows -i "test_windows.py,test_darwin.py" tests/proxy || exit 1; \
11 fi
12 rm -rf _trial_temp
13
14=== modified file 'run-tests.bat'
15--- run-tests.bat 2012-05-16 16:56:41 +0000
16+++ run-tests.bat 2012-07-19 14:21:30 +0000
17@@ -74,7 +74,7 @@
18 COPY windows\clientdefs.py ubuntuone\clientdefs.py
19 COPY windows\logging.conf data\logging.conf
20 :: execute the tests with a number of ignored linux and mac os only modules
21-"%PYTHONEXEPATH%" "%TRIALPATH%" --reactor=twisted -c -p tests\platform\linux -i "test_linux.py,test_darwin.py" %PARAMS% tests
22+"%PYTHONEXEPATH%" "%TRIALPATH%" --reactor=twisted -c -p tests\platform\linux -i "test_linux.py,test_darwin.py,test_fsevents_daemon.py" %PARAMS% tests
23
24 IF %SKIPLINT% == 1 (
25 ECHO Skipping style checks
26
27=== added file 'tests/platform/filesystem_notifications/test_fsevents_daemon.py'
28--- tests/platform/filesystem_notifications/test_fsevents_daemon.py 1970-01-01 00:00:00 +0000
29+++ tests/platform/filesystem_notifications/test_fsevents_daemon.py 2012-07-19 14:21:30 +0000
30@@ -0,0 +1,437 @@
31+# -*- coding: utf-8 *-*
32+#
33+# Copyright 2012 Canonical Ltd.
34+#
35+# This program is free software: you can redistribute it and/or modify it
36+# under the terms of the GNU General Public License version 3, as published
37+# by the Free Software Foundation.
38+#
39+# This program is distributed in the hope that it will be useful, but
40+# WITHOUT ANY WARRANTY; without even the implied warranties of
41+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
42+# PURPOSE. See the GNU General Public License for more details.
43+#
44+# You should have received a copy of the GNU General Public License along
45+# with this program. If not, see <http://www.gnu.org/licenses/>.
46+#
47+# In addition, as a special exception, the copyright holders give
48+# permission to link the code of portions of this program with the
49+# OpenSSL library under certain conditions as described in each
50+# individual source file, and distribute linked combinations
51+# including the two.
52+# You must obey the GNU General Public License in all respects
53+# for all of the code used other than OpenSSL. If you modify
54+# file(s) with this exception, you may extend this exception to your
55+# version of the file(s), but you are not obligated to do so. If you
56+# do not wish to do so, delete this exception statement from your
57+# version. If you delete this exception statement from all source
58+# files in the program, then also delete it here.
59+"""Tests for the fsevents daemon integration."""
60+
61+import os
62+
63+from twisted.internet import defer
64+
65+from contrib.testing.testcase import BaseTwistedTestCase
66+from ubuntuone.darwin import fsevents
67+from ubuntuone.platform.filesystem_notifications.darwin import fsevents_daemon
68+from ubuntuone.platform.filesystem_notifications.pyinotify_agnostic import (
69+ IN_CREATE,
70+ IN_DELETE,
71+ IN_MOVED_FROM,
72+ IN_MOVED_TO,
73+)
74+
75+class FakeDaemonEvent(object):
76+ """A fake daemon event."""
77+
78+ def __init__(self):
79+ self.event_paths = []
80+ self.is_directory = False
81+ self.event_type = None
82+
83+
84+class FakeProcessor(object):
85+ """A fake processor."""
86+
87+ def __init__(self):
88+ """Create a new instance."""
89+ self.processed_events = []
90+
91+ def __call__(self, event):
92+ """Process and event."""
93+ self.processed_events.append(event)
94+
95+
96+class FakePyInotifyEventsFactory(object):
97+ """Fake factory."""
98+
99+ def __init__(self, processor):
100+ """Create a new instance."""
101+ self.processor = processor
102+ self.called = []
103+ self.watched_paths = []
104+ self.ignored_paths = []
105+
106+
107+class FakeTransport(object):
108+ """A fake transport for the protocol."""
109+
110+ def __init__(self):
111+ """Create a new instance."""
112+ self.called = []
113+
114+ def loseConnection(self):
115+ """"Lost the connection."""
116+ self.called.append('loseConnection')
117+
118+
119+class FakeProtocol(object):
120+ """A fake protocol object to interact with the daemon."""
121+
122+ def __init__(self):
123+ """Create a new instance."""
124+ self.called = []
125+ self.transport = FakeTransport()
126+
127+ def remove_user(self):
128+ """Remove the user."""
129+ self.called.append('remove_user')
130+ return defer.succeed(None)
131+
132+ def remove_path(self, path):
133+ """Remove a path."""
134+ self.called.extend(['remove_path', path])
135+ return defer.succeed(True)
136+
137+ def add_path(self, path):
138+ """Add a path."""
139+ self.called.extend(['add_path', path])
140+
141+class PyInotifyEventsFactoryTestCase(BaseTwistedTestCase):
142+ """Test the factory used to receive events."""
143+
144+ @defer.inlineCallbacks
145+ def setUp(self):
146+ """Set the diff tests."""
147+ yield super(PyInotifyEventsFactoryTestCase, self).setUp()
148+ self.processor = FakeProcessor()
149+ self.factory = fsevents_daemon.PyInotifyEventsFactory(self.processor)
150+
151+ def test_path_interesting_not_watched_or_ignored(self):
152+ """Test that we do know if the path is not interesting."""
153+ path = u'/not/watched/path'
154+ self.assertTrue(self.factory.path_is_not_interesting(path))
155+
156+ def test_path_interesting_watched_not_ignored(self):
157+ """Test that we do not know if the path is not interesting."""
158+ path = u'/watched/path'
159+ self.factory.watched_paths.append(path)
160+ self.assertFalse(self.factory.path_is_not_interesting(path))
161+
162+ def test_path_interesting_watched_but_ignored(self):
163+ """Test that we do not know if the path is not interesting."""
164+ path = u'/ignored/path'
165+ self.factory.watched_paths.append(path)
166+ self.factory.ignored_paths.append(path)
167+ self.assertTrue(self.factory.path_is_not_interesting(path))
168+
169+ def test_path_interesting_not_watched_but_ignored(self):
170+ """Test that we do not know if the path is not interesting."""
171+ path = u'/ignored/path'
172+ self.factory.ignored_paths.append(path)
173+ self.assertTrue(self.factory.path_is_not_interesting(path))
174+
175+ def test_is_create_false_rename(self):
176+ """Test if we do know when an event is a create."""
177+ source_path = u'/other/watched/path'
178+ destination_path = u'/watched/path'
179+ source_head, _ = os.path.split(source_path)
180+ destination_head, _ = os.path.split(destination_path)
181+ self.factory.watched_paths.extend([source_head, destination_head])
182+ event = FakeDaemonEvent()
183+ event.event_paths.extend([source_path, destination_path])
184+ self.assertFalse(self.factory.is_create(event))
185+
186+ def test_is_create_false_delete(self):
187+ """Test if we do know when an event is a create."""
188+ source_path = u'/watched/path'
189+ destination_path = u'/not/watched/path'
190+ source_head, _ = os.path.split(source_path)
191+ self.factory.watched_paths.append(source_head)
192+ event = FakeDaemonEvent()
193+ event.event_paths.extend([source_path, destination_path])
194+ self.assertFalse(self.factory.is_create(event))
195+
196+ def test_is_create_true(self):
197+ """Test is we do know when an event is a create."""
198+ source_path = u'/not/watched/path'
199+ destination_path = u'/watched/path'
200+ destination_head, _ = os.path.split(destination_path)
201+ self.factory.watched_paths.append(destination_head)
202+ event = FakeDaemonEvent()
203+ event.event_paths.extend([source_path, destination_path])
204+ self.assertTrue(self.factory.is_create(event))
205+
206+ def test_is_delete_false_rename(self):
207+ """Test if we do know when an event is a delete."""
208+ source_path = u'/other/watched/path'
209+ destination_path = u'/watched/path'
210+ source_head, _ = os.path.split(source_path)
211+ destination_head, _ = os.path.split(destination_path)
212+ self.factory.watched_paths.extend([source_head, destination_head])
213+ event = FakeDaemonEvent()
214+ event.event_paths.extend([source_path, destination_path])
215+ self.assertFalse(self.factory.is_delete(event))
216+
217+ def test_is_delete_false_create(self):
218+ """Test if we do know when an event is a delete."""
219+ source_path = u'/not/watched/path'
220+ destination_path = u'/watched/path'
221+ destination_head, _ = os.path.split(destination_path)
222+ self.factory.watched_paths.append(destination_head)
223+ event = FakeDaemonEvent()
224+ event.event_paths.extend([source_path, destination_path])
225+ self.assertFalse(self.factory.is_delete(event))
226+
227+ def test_is_delete_true(self):
228+ """Test if we do know when an event is a delete."""
229+ source_path = u'/watched/path'
230+ destination_path = u'/not/watched/path'
231+ source_head, _ = os.path.split(source_path)
232+ self.factory.watched_paths.append(source_head)
233+ event = FakeDaemonEvent()
234+ event.event_paths.extend([source_path, destination_path])
235+ self.assertTrue(self.factory.is_delete(event))
236+
237+ def test_generate_from_event(self):
238+ """Test the creation of a fake from event."""
239+ cookie = 'cookie'
240+ source_path = u'/source/path'
241+ destination_path = u'/destination/path'
242+ event = FakeDaemonEvent()
243+ event.event_paths.extend([source_path, destination_path])
244+ pyinotify_event = self.factory.generate_from_event(event, cookie)
245+ self.assertEqual(cookie, pyinotify_event.cookie)
246+ self.assertEqual(0, pyinotify_event.wd)
247+ self.assertEqual(event.is_directory, pyinotify_event.dir)
248+ self.assertEqual(IN_MOVED_FROM, pyinotify_event.mask)
249+ self.assertEqual(source_path, pyinotify_event.pathname)
250+
251+ def test_generate_to_event(self):
252+ """Test the creation of a fake to event."""
253+ cookie = 'cookie'
254+ source_path = u'/source/path'
255+ destination_path = u'/destination/path'
256+ event = FakeDaemonEvent()
257+ event.event_paths.extend([source_path, destination_path])
258+ pyinotify_event = self.factory.generate_to_event(event, cookie)
259+ self.assertEqual(cookie, pyinotify_event.cookie)
260+ self.assertEqual(0, pyinotify_event.wd)
261+ self.assertEqual(event.is_directory, pyinotify_event.dir)
262+ self.assertEqual(IN_MOVED_TO, pyinotify_event.mask)
263+ self.assertEqual(destination_path, pyinotify_event.pathname)
264+
265+ def test_convert_in_pyinotify_event_no_rename(self):
266+ """Test the creation of a no rename event."""
267+ event_path = u'/path/of/the/event'
268+ for action in fsevents_daemon.DARWIN_ACTIONS:
269+ event = FakeDaemonEvent()
270+ event.event_paths.append(event_path)
271+ event.event_type = action
272+ converted_events = self.factory.convert_in_pyinotify_event(event)
273+ self.assertEqual(1, len(converted_events))
274+ pyinotify_event = converted_events[0]
275+ self.assertEqual(0, pyinotify_event.wd)
276+ self.assertEqual(event.is_directory, pyinotify_event.dir)
277+ self.assertEqual(fsevents_daemon.DARWIN_ACTIONS[action],
278+ pyinotify_event.mask)
279+ self.assertEqual(event_path, pyinotify_event.pathname)
280+
281+ def test_convert_in_pyinotify_event_rename_create(self):
282+ """Test the creation of a rename which is a create."""
283+ source_path = u'/not/watched/path'
284+ destination_path = u'/watched/path'
285+ head, _ = os.path.split(destination_path)
286+ self.factory.watched_paths.append(head)
287+ event = FakeDaemonEvent()
288+ event.event_type = fsevents.FSE_RENAME
289+ event.event_paths.extend([source_path, destination_path])
290+ converted_events = self.factory.convert_in_pyinotify_event(event)
291+ self.assertEqual(1, len(converted_events))
292+ pyinotify_event = converted_events[0]
293+ self.assertEqual(0, pyinotify_event.wd)
294+ self.assertEqual(event.is_directory, pyinotify_event.dir)
295+ self.assertEqual(IN_CREATE, pyinotify_event.mask)
296+ self.assertEqual(destination_path, pyinotify_event.pathname)
297+
298+ def test_convert_in_pyinotify_event_rename_delete(self):
299+ """Test the creation of a rename which is a delete."""
300+ source_path = u'/watched/path'
301+ destination_path = u'/not/watched/path'
302+ head, _ = os.path.split(source_path)
303+ self.factory.watched_paths.append(head)
304+ event = FakeDaemonEvent()
305+ event.event_type = fsevents.FSE_RENAME
306+ event.event_paths.extend([source_path, destination_path])
307+ converted_events = self.factory.convert_in_pyinotify_event(event)
308+ self.assertEqual(1, len(converted_events))
309+ pyinotify_event = converted_events[0]
310+ self.assertEqual(0, pyinotify_event.wd)
311+ self.assertEqual(event.is_directory, pyinotify_event.dir)
312+ self.assertEqual(IN_DELETE, pyinotify_event.mask)
313+ self.assertEqual(source_path, pyinotify_event.pathname)
314+
315+ def test_convert_in_pyinotify_event_rename(self):
316+ """Test the creation of a rename event."""
317+ source_path = u'/watched/path1'
318+ destination_path = u'/watched/path2'
319+ head, _ = os.path.split(source_path)
320+ self.factory.watched_paths.append(head)
321+ event = FakeDaemonEvent()
322+ event.event_type = fsevents.FSE_RENAME
323+ event.event_paths.extend([source_path, destination_path])
324+ converted_events = self.factory.convert_in_pyinotify_event(event)
325+ self.assertEqual(2, len(converted_events))
326+ from_event = converted_events[0]
327+ to_event = converted_events[1]
328+ # assert from event
329+ self.assertEqual(0, from_event.wd)
330+ self.assertEqual(event.is_directory, from_event.dir)
331+ self.assertEqual(IN_MOVED_FROM, from_event.mask)
332+ self.assertEqual(source_path, from_event.pathname)
333+ # assert to event
334+ self.assertEqual(0, to_event.wd)
335+ self.assertEqual(event.is_directory, to_event.dir)
336+ self.assertEqual(IN_MOVED_TO, to_event.mask)
337+ self.assertEqual(destination_path, to_event.pathname)
338+ # assert cookie
339+ self.assertEqual(from_event.cookie, to_event.cookie)
340+
341+ def test_process_event_ignored_type(self):
342+ """Test processing the event of an ignored type."""
343+ for action in fsevents_daemon.DARWIN_IGNORED_ACTIONS:
344+ event = FakeDaemonEvent()
345+ event.event_type = action
346+ self.factory.process_event(event)
347+ self.assertEqual(0, len(self.processor.processed_events))
348+
349+ def test_process_event_dropped(self):
350+ """Test processing the drop of the events."""
351+ func_called = []
352+ event = FakeDaemonEvent()
353+ event.event_type = fsevents.FSE_EVENTS_DROPPED
354+
355+ def fake_events_dropped():
356+ """A fake events dropped implementation."""
357+ func_called.append('fake_events_dropped')
358+
359+ self.patch(self.factory, 'events_dropper', fake_events_dropped)
360+ self.factory.process_event(event)
361+ self.assertIn('fake_events_dropped', func_called)
362+
363+ def test_process_ignored_path(self):
364+ """Test processing events from an ignored path."""
365+ event_path = u'/path/of/the/event'
366+ head, _ = os.path.split(event_path)
367+ self.factory.ignored_paths.append(head)
368+ event = FakeDaemonEvent()
369+ event.event_paths.append(event_path)
370+ event.event_type = fsevents.FSE_CREATE_FILE
371+ self.factory.process_event(event)
372+ self.assertEqual(0, len(self.processor.processed_events))
373+
374+ def test_process_not_ignored_path(self):
375+ """Test processing events that are not ignored."""
376+ event_path = u'/path/of/the/event'
377+ head, _ = os.path.split(event_path)
378+ self.factory.watched_paths.append(head)
379+ event = FakeDaemonEvent()
380+ event.event_paths.append(event_path)
381+ event.event_type = fsevents.FSE_CREATE_FILE
382+ self.factory.process_event(event)
383+ self.assertEqual(1, len(self.processor.processed_events))
384+ self.assertEqual(event_path,
385+ self.processor.processed_events[0].pathname)
386+
387+
388+class FilesystemMonitorTestCase(BaseTwistedTestCase):
389+ """Test the notify processor."""
390+
391+ def fake_connect_to_daemon(self):
392+ """A fake connection to daemon call."""
393+ self.monitor._protocol = self.protocol
394+ defer.succeed(self.protocol)
395+
396+ @defer.inlineCallbacks
397+ def setUp(self):
398+ """Set the tests."""
399+ yield super(FilesystemMonitorTestCase, self).setUp()
400+ self.processor = FakeProcessor()
401+ self.factory = FakePyInotifyEventsFactory(self.processor)
402+ self.protocol = FakeProtocol()
403+ self.monitor = fsevents_daemon.FilesystemMonitor(None, None)
404+
405+ # override default objects
406+ self.monitor._processor = self.processor
407+ self.monitor._factory = self.factory
408+
409+ # patch the connect
410+ self.patch(fsevents_daemon.FilesystemMonitor, '_connect_to_daemon',
411+ self.fake_connect_to_daemon)
412+
413+ @defer.inlineCallbacks
414+ def test_shutdown_protocol(self):
415+ """Test shutdown with a protocol."""
416+ self.monitor._protocol = self.protocol
417+ yield self.monitor.shutdown()
418+ self.assertIn('remove_user', self.protocol.called)
419+
420+ @defer.inlineCallbacks
421+ def test_shutdown_no_protocol(self):
422+ """Test shutdown without a protocol."""
423+ stopped = yield self.monitor.shutdown()
424+ self.assertTrue(stopped)
425+
426+ @defer.inlineCallbacks
427+ def test_rm_path_not_root(self):
428+ """Test removing a path."""
429+ dirpath = '/path/to/remove/'
430+ self.factory.watched_paths.append('/path')
431+ yield self.monitor.rm_watch(dirpath)
432+ self.assertIn(dirpath, self.factory.ignored_paths)
433+
434+ @defer.inlineCallbacks
435+ def test_rm_path_root(self):
436+ """Test removing a path that is a root path."""
437+ dirpath = '/path/to/remove/'
438+ self.factory.watched_paths.append(dirpath)
439+ yield self.monitor.rm_watch(dirpath)
440+ self.assertIn('remove_path', self.protocol.called)
441+ self.assertIn(dirpath, self.protocol.called)
442+ self.assertNotIn(dirpath, self.factory.watched_paths)
443+
444+ @defer.inlineCallbacks
445+ def test_add_watch_not_root(self):
446+ """Test adding a watch."""
447+ dirpath = '/path/to/remove/'
448+ self.factory.watched_paths.append('/path')
449+ yield self.monitor.add_watch(dirpath)
450+ self.assertNotIn('add_path', self.protocol.called)
451+
452+ @defer.inlineCallbacks
453+ def test_add_watch_root(self):
454+ """Test adding a watch that is a root."""
455+ dirpath = '/path/to/remove/'
456+ self.factory.watched_paths.append('/other/path')
457+ yield self.monitor.add_watch(dirpath)
458+ self.assertIn('add_path', self.protocol.called)
459+ self.assertIn(dirpath, self.protocol.called)
460+
461+ @defer.inlineCallbacks
462+ def test_add_watch_ignored(self):
463+ """Test adding a watch that was ignored."""
464+ dirpath = '/path/to/remove/'
465+ self.factory.ignored_paths.append(dirpath)
466+ yield self.monitor.add_watch(dirpath)
467+ self.assertNotIn('add_path', self.protocol.called)
468
469=== modified file 'ubuntuone/platform/filesystem_notifications/common.py'
470--- ubuntuone/platform/filesystem_notifications/common.py 2012-07-13 12:39:33 +0000
471+++ ubuntuone/platform/filesystem_notifications/common.py 2012-07-19 14:21:30 +0000
472@@ -58,8 +58,10 @@
473 )
474
475 if sys.platform == 'darwin':
476- from ubuntuone.platform.filesystem_notifications import darwin
477- source = darwin
478+ from ubuntuone.platform.filesystem_notifications.darwin import (
479+ fsevents_client,
480+ )
481+ source = fsevents_client
482 elif sys.platform == 'win32':
483 from ubuntuone.platform.filesystem_notifications import windows
484 source = windows
485@@ -330,8 +332,8 @@
486 @is_valid_os_path(path_indexes=[1])
487 def get_wd(self, path):
488 """Return the watcher that is used to watch the given path."""
489- if not path.endswith(os.path.sep):
490- path = path + os.path.sep
491+ if not path[-1] == os.path.sep:
492+ path += os.path.sep
493 for current_wd in self._wdm:
494 watch_path = self._wdm[current_wd].path
495 if ((watch_path == path or watch_path in path)
496
497=== added directory 'ubuntuone/platform/filesystem_notifications/darwin'
498=== renamed file 'ubuntuone/platform/filesystem_notifications/darwin.py' => 'ubuntuone/platform/filesystem_notifications/darwin/__init__.py'
499--- ubuntuone/platform/filesystem_notifications/darwin.py 2012-07-13 12:39:33 +0000
500+++ ubuntuone/platform/filesystem_notifications/darwin/__init__.py 2012-07-19 14:21:30 +0000
501@@ -28,132 +28,8 @@
502 # files in the program, then also delete it here.
503 """Filesystem Notifications module for MAC OS."""
504
505-import os
506-
507-import fsevents
508-from twisted.internet import defer, reactor
509-
510-from ubuntuone.platform.filesystem_notifications.pyinotify_agnostic import (
511- IN_DELETE,
512- IN_CREATE,
513- IN_MODIFY,
514- IN_MOVED_FROM,
515- IN_MOVED_TO,
516-)
517-
518-# a map between the few events that we have on common platforms and those
519-# found in pyinotify
520-ACTIONS = {
521- fsevents.IN_CREATE: IN_CREATE,
522- fsevents.IN_DELETE: IN_DELETE,
523- fsevents.IN_MODIFY: IN_MODIFY,
524- fsevents.IN_MOVED_FROM: IN_MOVED_FROM,
525- fsevents.IN_MOVED_TO: IN_MOVED_TO,
526-}
527-
528-# a map of the actions to names so that we have better logs.
529-ACTIONS_NAMES = {
530- fsevents.IN_CREATE: 'IN_CREATE',
531- fsevents.IN_DELETE: 'IN_DELETE',
532- fsevents.IN_MODIFY: 'IN_MODIFY',
533- fsevents.IN_MOVED_FROM: 'IN_MOVED_FROM',
534- fsevents.IN_MOVED_TO: 'IN_MOVED_TO',
535-}
536-
537-
538-def path_is_ignored(path):
539- """Should we ignore this path in the current platform.?"""
540- # don't support links yet
541- if os.path.islink(path):
542- return True
543- return False
544-
545-
546-# The implementation of the code that is provided as the pyinotify substitute
547-class Watch(object):
548- """Implement the same functions as pyinotify.Watch."""
549-
550- def __init__(self, path, process_events):
551- """Create a new instance for the given path.
552-
553- The process_events parameter is a callback to be executed in the main
554- reactor thread to convert events in pyinotify events and add them to
555- the state machine.
556- """
557- self.path = os.path.abspath(path)
558- self.process_events = process_events
559- self.watching = False
560- self.ignore_paths = []
561- # Create stream with folder to watch
562- self.stream = fsevents.Stream(self._process_events,
563- path, file_events=True)
564-
565- def _process_events(self, event):
566- """Receive the filesystem event and move it to the main thread."""
567- reactor.callFromThread(self._process_events_in_main_thread, event)
568-
569- def _process_events_in_main_thread(self, event):
570- """Process the events from the queue."""
571- action, cookie, file_name = (event.mask, event.cookie, event.name)
572-
573- syncdaemon_path = os.path.join(self.path, file_name)
574- self.process_events(action, file_name, cookie,
575- syncdaemon_path)
576-
577- def start_watching(self):
578- """Start watching."""
579- self.watching = True
580- return defer.succeed(self.watching)
581-
582- def stop_watching(self):
583- """Stop watching."""
584- self.watching = False
585- return defer.succeed(self.watching)
586-
587- # For API compatibility
588- @property
589- def started(self):
590- """A deferred that will be called when the watch is running."""
591- return defer.succeed(self.watching)
592-
593- @property
594- def stopped(self):
595- """A deferred fired when the watch thread has finished."""
596- return defer.succeed(self.watching)
597-
598-
599-class WatchManager(object):
600- """Implement the same functions as pyinotify.WatchManager.
601-
602- All paths passed to methods in this class should be darwin paths.
603-
604- """
605-
606- def __init__(self, log):
607- """Init the manager to keep track of the different watches."""
608- self.log = log
609- self.observer = fsevents.Observer()
610- self.observer.start()
611-
612- def stop_watch(self, watch):
613- """Stop a given watch."""
614- watch.stop_watching()
615- self.observer.unschedule(watch.platform_watch.stream)
616- return defer.succeed(True)
617-
618- def stop(self):
619- """Stop the manager."""
620- self.observer.stop()
621- self.observer.join()
622-
623- def del_watch(self, watch):
624- """Delete the watch and clean resources."""
625- self.observer.unschedule(watch.platform_watch.stream)
626-
627- def add_watch(self, watch):
628- """This method perform actually the action of registering the watch."""
629- self.observer.schedule(watch.platform_watch.stream)
630- return True
631-
632- def rm_watch(self, watch):
633- """Remove the the watch with the given wd."""
634+from ubuntuone.platform.filesystem_notifications.darwin import fsevents_daemon
635+
636+
637+FilesystemMonitor = fsevents_daemon.FilesystemMonitor
638+NotifyProcessor = fsevents_daemon.NotifyProcessor
639
640=== added file 'ubuntuone/platform/filesystem_notifications/darwin/fsevents_client.py'
641--- ubuntuone/platform/filesystem_notifications/darwin/fsevents_client.py 1970-01-01 00:00:00 +0000
642+++ ubuntuone/platform/filesystem_notifications/darwin/fsevents_client.py 2012-07-19 14:21:30 +0000
643@@ -0,0 +1,159 @@
644+# -*- coding: utf-8 *-*
645+#
646+# Copyright 2012 Canonical Ltd.
647+#
648+# This program is free software: you can redistribute it and/or modify it
649+# under the terms of the GNU General Public License version 3, as published
650+# by the Free Software Foundation.
651+#
652+# This program is distributed in the hope that it will be useful, but
653+# WITHOUT ANY WARRANTY; without even the implied warranties of
654+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
655+# PURPOSE. See the GNU General Public License for more details.
656+#
657+# You should have received a copy of the GNU General Public License along
658+# with this program. If not, see <http://www.gnu.org/licenses/>.
659+#
660+# In addition, as a special exception, the copyright holders give
661+# permission to link the code of portions of this program with the
662+# OpenSSL library under certain conditions as described in each
663+# individual source file, and distribute linked combinations
664+# including the two.
665+# You must obey the GNU General Public License in all respects
666+# for all of the code used other than OpenSSL. If you modify
667+# file(s) with this exception, you may extend this exception to your
668+# version of the file(s), but you are not obligated to do so. If you
669+# do not wish to do so, delete this exception statement from your
670+# version. If you delete this exception statement from all source
671+# files in the program, then also delete it here.
672+"""Filesystem Notifications module for MAC OS."""
673+
674+import os
675+
676+import fsevents
677+from twisted.internet import defer, reactor
678+
679+from ubuntuone.platform.filesystem_notifications.pyinotify_agnostic import (
680+ IN_DELETE,
681+ IN_CREATE,
682+ IN_MODIFY,
683+ IN_MOVED_FROM,
684+ IN_MOVED_TO,
685+)
686+
687+# a map between the few events that we have on common platforms and those
688+# found in pyinotify
689+ACTIONS = {
690+ fsevents.IN_CREATE: IN_CREATE,
691+ fsevents.IN_DELETE: IN_DELETE,
692+ fsevents.IN_MODIFY: IN_MODIFY,
693+ fsevents.IN_MOVED_FROM: IN_MOVED_FROM,
694+ fsevents.IN_MOVED_TO: IN_MOVED_TO,
695+}
696+
697+# a map of the actions to names so that we have better logs.
698+ACTIONS_NAMES = {
699+ fsevents.IN_CREATE: 'IN_CREATE',
700+ fsevents.IN_DELETE: 'IN_DELETE',
701+ fsevents.IN_MODIFY: 'IN_MODIFY',
702+ fsevents.IN_MOVED_FROM: 'IN_MOVED_FROM',
703+ fsevents.IN_MOVED_TO: 'IN_MOVED_TO',
704+}
705+
706+
707+def path_is_ignored(path):
708+ """Should we ignore this path in the current platform.?"""
709+ # don't support links yet
710+ if os.path.islink(path):
711+ return True
712+ return False
713+
714+
715+# The implementation of the code that is provided as the pyinotify substitute
716+class Watch(object):
717+ """Implement the same functions as pyinotify.Watch."""
718+
719+ def __init__(self, path, process_events):
720+ """Create a new instance for the given path.
721+
722+ The process_events parameter is a callback to be executed in the main
723+ reactor thread to convert events in pyinotify events and add them to
724+ the state machine.
725+ """
726+ self.path = os.path.abspath(path)
727+ self.process_events = process_events
728+ self.watching = False
729+ self.ignore_paths = []
730+ # Create stream with folder to watch
731+ self.stream = fsevents.Stream(self._process_events,
732+ path, file_events=True)
733+
734+ def _process_events(self, event):
735+ """Receive the filesystem event and move it to the main thread."""
736+ reactor.callFromThread(self._process_events_in_main_thread, event)
737+
738+ def _process_events_in_main_thread(self, event):
739+ """Process the events from the queue."""
740+ action, cookie, file_name = (event.mask, event.cookie, event.name)
741+
742+ syncdaemon_path = os.path.join(self.path, file_name)
743+ self.process_events(action, file_name, cookie,
744+ syncdaemon_path)
745+
746+ def start_watching(self):
747+ """Start watching."""
748+ self.watching = True
749+ return defer.succeed(self.watching)
750+
751+ def stop_watching(self):
752+ """Stop watching."""
753+ self.watching = False
754+ return defer.succeed(self.watching)
755+
756+ # For API compatibility
757+ @property
758+ def started(self):
759+ """A deferred that will be called when the watch is running."""
760+ return defer.succeed(self.watching)
761+
762+ @property
763+ def stopped(self):
764+ """A deferred fired when the watch thread has finished."""
765+ return defer.succeed(self.watching)
766+
767+
768+class WatchManager(object):
769+ """Implement the same functions as pyinotify.WatchManager.
770+
771+ All paths passed to methods in this class should be darwin paths.
772+
773+ """
774+
775+ def __init__(self, log):
776+ """Init the manager to keep track of the different watches."""
777+ self.log = log
778+ self.observer = fsevents.Observer()
779+ self.observer.start()
780+
781+ def stop_watch(self, watch):
782+ """Stop a given watch."""
783+ watch.stop_watching()
784+ self.observer.unschedule(watch.platform_watch.stream)
785+ return defer.succeed(True)
786+
787+ def stop(self):
788+ """Stop the manager."""
789+ self.observer.stop()
790+ self.observer.join()
791+
792+ def del_watch(self, watch):
793+ """Delete the watch and clean resources."""
794+ self.observer.unschedule(watch.platform_watch.stream)
795+
796+ def add_watch(self, watch):
797+ """This method perform actually the action of registering the watch."""
798+ self.observer.schedule(watch.platform_watch.stream)
799+ return True
800+
801+ def rm_watch(self, watch):
802+ """Remove the the watch with the given wd."""
803
804=== added file 'ubuntuone/platform/filesystem_notifications/darwin/fsevents_daemon.py'
805--- ubuntuone/platform/filesystem_notifications/darwin/fsevents_daemon.py 1970-01-01 00:00:00 +0000
806+++ ubuntuone/platform/filesystem_notifications/darwin/fsevents_daemon.py 2012-07-19 14:21:30 +0000
807@@ -0,0 +1,594 @@
808+# -*- coding: utf-8 *-*
809+#
810+# Copyright 2012 Canonical Ltd.
811+#
812+# This program is free software: you can redistribute it and/or modify it
813+# under the terms of the GNU General Public License version 3, as published
814+# by the Free Software Foundation.
815+#
816+# This program is distributed in the hope that it will be useful, but
817+# WITHOUT ANY WARRANTY; without even the implied warranties of
818+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
819+# PURPOSE. See the GNU General Public License for more details.
820+#
821+# You should have received a copy of the GNU General Public License along
822+# with this program. If not, see <http://www.gnu.org/licenses/>.
823+#
824+# permission to link the code of portions of this program with the
825+# OpenSSL library under certain conditions as described in each
826+# individual source file, and distribute linked combinations
827+# including the two.
828+# You must obey the GNU General Public License in all respects
829+# for all of the code used other than OpenSSL. If you modify
830+# file(s) with this exception, you may extend this exception to your
831+# version of the file(s), but you are not obligated to do so. If you
832+# do not wish to do so, delete this exception statement from your
833+# version. If you delete this exception statement from all source
834+# files in the program, then also delete it here.
835+"""Filesystem notifications based on the fsevents daemon.."""
836+
837+import logging
838+import os
839+import unicodedata
840+
841+from uuid import uuid4
842+
843+from twisted.internet import defer, endpoints, reactor
844+
845+from ubuntuone import logger
846+from ubuntuone.darwin import fsevents
847+from ubuntuone.platform.filesystem_notifications.pyinotify_agnostic import (
848+ Event,
849+ ProcessEvent,
850+ IN_OPEN,
851+ IN_CLOSE_NOWRITE,
852+ IN_CLOSE_WRITE,
853+ IN_CREATE,
854+ IN_IGNORED,
855+ IN_ISDIR,
856+ IN_DELETE,
857+ IN_MOVED_FROM,
858+ IN_MOVED_TO,
859+ IN_MODIFY)
860+
861+TRACE = logger.TRACE
862+
863+# map the fsevents actions to those from pyinotify
864+DARWIN_ACTIONS = {
865+ fsevents.FSE_CREATE_FILE: IN_CREATE,
866+ fsevents.FSE_DELETE: IN_DELETE,
867+ fsevents.FSE_STAT_CHANGED: IN_MODIFY,
868+ fsevents.FSE_CONTENT_MODIFIED: IN_MODIFY,
869+ fsevents.FSE_CREATE_DIR: IN_CREATE,
870+}
871+
872+# list of those events from which we do not care
873+DARWIN_IGNORED_ACTIONS = (
874+ fsevents.FSE_UNKNOWN,
875+ fsevents.FSE_INVALID,
876+ fsevents.FSE_EXCHANGE,
877+ fsevents.FSE_FINDER_INFO_CHANGED,
878+ fsevents.FSE_CHOWN,
879+ fsevents.FSE_XATTR_MODIFIED,
880+ fsevents.FSE_XATTR_REMOVED,
881+)
882+
883+# translates quickly the event and it's is_dir state to our standard events
884+NAME_TRANSLATIONS = {
885+ IN_OPEN: 'FS_FILE_OPEN',
886+ IN_CLOSE_NOWRITE: 'FS_FILE_CLOSE_NOWRITE',
887+ IN_CLOSE_WRITE: 'FS_FILE_CLOSE_WRITE',
888+ IN_CREATE: 'FS_FILE_CREATE',
889+ IN_CREATE | IN_ISDIR: 'FS_DIR_CREATE',
890+ IN_DELETE: 'FS_FILE_DELETE',
891+ IN_DELETE | IN_ISDIR: 'FS_DIR_DELETE',
892+ IN_MOVED_FROM: 'FS_FILE_DELETE',
893+ IN_MOVED_FROM | IN_ISDIR: 'FS_DIR_DELETE',
894+ IN_MOVED_TO: 'FS_FILE_CREATE',
895+ IN_MOVED_TO | IN_ISDIR: 'FS_DIR_CREATE'}
896+
897+# TODO: This should be in fsevents to be imported!
898+# Path to the socket used by the daemon
899+DAEMON_SOCKET = '/var/run/ubuntuone_fsevents_daemon'
900+
901+def get_syncdaemon_valid_path(path):
902+ """Return a valid encoded path."""
903+ return unicodedata.normalize('NFC', path).encode('utf-8')
904+
905+
906+class PyInotifyEventsFactory(fsevents.FsEventsFactory):
907+ """Factory that process events and converts them in pyinotify ones."""
908+
909+ def __init__(self, processor, ignored_events=DARWIN_IGNORED_ACTIONS):
910+ """Create a new instance."""
911+ # old style class
912+ fsevents.FsEventsFactory.__init__(self)
913+ self._processor = processor
914+ self._ignored_events = ignored_events
915+ self.watched_paths = []
916+ self.ignored_paths = []
917+
918+ def events_dropper(self):
919+ """Deal with the fact that the daemon dropped events."""
920+
921+ def path_is_not_interesting(self, path):
922+ """Return if the factory is interested in the path."""
923+ is_watched = any(path.startswith(watched_path)
924+ for watched_path in self.watched_paths)
925+ is_ignored = any(path.startswith(ignored_path)
926+ for ignored_path in self.ignored_paths)
927+ return not is_watched or (is_watched and is_ignored)
928+
929+ def is_create(self, event):
930+ """Decide if a rename event should be considered a create."""
931+ # is a create if the creation path (first path) is either not watched or
932+ # in the ignored paths
933+ source_path = get_syncdaemon_valid_path(event.event_paths[0])
934+ return self.path_is_not_interesting(source_path)
935+
936+ def is_delete(self, event):
937+ """Decide if a rename event should be considered a delete."""
938+ # is a delete if the destination path (second path) is either not
939+ # watched or in the ignored paths
940+ dest_path = get_syncdaemon_valid_path(event.event_paths[1])
941+ return self.path_is_not_interesting(dest_path)
942+
943+ def generate_from_event(self, event, cookie):
944+ """Return a fake from event from a rename one."""
945+ source_path = get_syncdaemon_valid_path(event.event_paths[0])
946+ mask = IN_MOVED_FROM
947+ if event.is_directory:
948+ mask |= IN_ISDIR
949+ head, tail = os.path.split(source_path)
950+ event_raw_data = {
951+ 'wd': 0, # we only have one factory
952+ 'dir': event.is_directory,
953+ 'mask': mask,
954+ 'name': tail,
955+ 'cookie': cookie,
956+ 'path': '.'}
957+ move_from_event = Event(event_raw_data)
958+ move_from_event.pathname = source_path
959+ return move_from_event
960+
961+ def generate_to_event(self, event, cookie):
962+ """Return a fake to event from a rename one."""
963+ source_path = get_syncdaemon_valid_path(event.event_paths[0])
964+ destination_path = get_syncdaemon_valid_path(event.event_paths[1])
965+ mask = IN_MOVED_TO
966+ if event.is_directory:
967+ mask |= IN_ISDIR
968+ source_head, source_tail = os.path.split(source_path)
969+ head, tail = os.path.split(destination_path)
970+ event_raw_data = {
971+ 'wd': 0, # we only have one factory
972+ 'dir': event.is_directory,
973+ 'mask': mask,
974+ 'name': tail,
975+ 'cookie': cookie,
976+ 'src_pathname' : source_tail,
977+ 'path': '.'}
978+ move_to_event = Event(event_raw_data)
979+ move_to_event.pathname = destination_path
980+ return move_to_event
981+
982+ def convert_in_pyinotify_event(self, event):
983+ """Get an event from the daemon and convert it in a pyinotify one."""
984+ # the rename is a special type of event because it has to be either
985+ # converted is a pair of events or in a single one (CREATE or DELETE)
986+ if event.event_type == fsevents.FSE_RENAME:
987+ is_create = self.is_create(event)
988+ if is_create or self.is_delete(event):
989+ mask = IN_CREATE if is_create else IN_DELETE
990+ if event.is_directory:
991+ mask |= IN_ISDIR
992+ # a create means that we moved from a not watched path to a
993+ # watched one and therefore we are interested in the SECOND
994+ # path of the event. A delete means that we moved from a
995+ # watched path for a not watched one and we care about the
996+ # FIRST path of the event
997+ path = event.event_paths[1] if is_create\
998+ else event.event_paths[0]
999+ path = get_syncdaemon_valid_path(path)
1000+ head, tail = os.path.split(path)
1001+ event_raw_data = {
1002+ 'wd': 0, # we only have one factory
1003+ 'dir': event.is_directory,
1004+ 'mask': mask,
1005+ 'name': tail,
1006+ 'path': '.'}
1007+ pyinotify_event = Event(event_raw_data)
1008+ pyinotify_event.pathname = path
1009+ return [pyinotify_event]
1010+ else:
1011+ # we do have a rename within watched paths lets generate to
1012+ # fake events
1013+ cookie = str(uuid4())
1014+ return [self.generate_from_event(event, cookie),
1015+ self.generate_to_event(event, cookie)]
1016+ else:
1017+ mask = DARWIN_ACTIONS[event.event_type]
1018+ if event.is_directory:
1019+ mask |= IN_ISDIR
1020+ # we do know that we are not dealing with a move which are the only
1021+ # events that have more than one path
1022+ path = get_syncdaemon_valid_path(event.event_paths[0])
1023+ head, tail = os.path.split(path)
1024+ event_raw_data = {
1025+ 'wd': 0, # we only have one factory
1026+ 'dir': event.is_directory,
1027+ 'mask': mask,
1028+ 'name': tail,
1029+ 'path': '.'}
1030+ pyinotify_event = Event(event_raw_data)
1031+ # FIXME: event deduces the pathname wrong and we need to manually
1032+ # set it
1033+ pyinotify_event.pathname = path
1034+ return [pyinotify_event]
1035+
1036+ def _is_ignored_path(self, path):
1037+ """Returns if the path is ignored."""
1038+ if not path[-1] == os.path.sep:
1039+ path += os.path.sep
1040+
1041+ is_ignored_child = any(ignored in path for ignored in self.ignored_paths)
1042+ return path in self.ignored_paths or is_ignored_child
1043+
1044+ def process_event(self, event):
1045+ """Process an event from the fsevent daemon."""
1046+ if event.event_type in self._ignored_events:
1047+ # Do nothing because sd does not care about such info
1048+ return
1049+ if event.event_type == fsevents.FSE_EVENTS_DROPPED:
1050+ # this should not be very common but we have to deal with it
1051+ return self.events_dropper()
1052+ events = self.convert_in_pyinotify_event(event)
1053+ for pyinotify_event in events:
1054+ # assert that the path name is valid
1055+ if not any([pyinotify_event.pathname.startswith(path)
1056+ for path in self.ignored_paths]):
1057+ # by definition we are being callFromThread so we do know that
1058+ # the events are executed in the right order \o/
1059+ if not self._is_ignored_path(pyinotify_event.pathname):
1060+ self._processor(pyinotify_event)
1061+
1062+
1063+class NotifyProcessor(ProcessEvent):
1064+ """Mac OS X implementation of NotifyProcessor"""
1065+
1066+ def __init__(self, monitor, ignore_config=None):
1067+ # XXX: avoid circular imports.
1068+ from ubuntuone.syncdaemon.filesystem_notifications import (
1069+ GeneralINotifyProcessor)
1070+ self.general_processor = GeneralINotifyProcessor(monitor,
1071+ self.handle_dir_delete, NAME_TRANSLATIONS,
1072+ self.platform_is_ignored, IN_IGNORED, ignore_config=ignore_config)
1073+ self.held_event = None
1074+
1075+ def rm_from_mute_filter(self, event, paths):
1076+ """Remove event from the mute filter."""
1077+ self.general_processor.rm_from_mute_filter(event, paths)
1078+
1079+ def add_to_mute_filter(self, event, paths):
1080+ """Add an event and path(s) to the mute filter."""
1081+ self.general_processor.add_to_mute_filter(event, paths)
1082+
1083+ def platform_is_ignored(self, path):
1084+ """Should we ignore this path in the current platform.?"""
1085+ # don't support links yet
1086+ if os.path.islink(path):
1087+ return True
1088+ return False
1089+
1090+ def is_ignored(self, path):
1091+ """Should we ignore this path?"""
1092+ return self.general_processor.is_ignored(path)
1093+
1094+ def release_held_event(self, timed_out=False):
1095+ """Release the event on hold to fulfill its destiny."""
1096+ self.general_processor.push_event(self.held_event)
1097+ self.held_event = None
1098+
1099+ def process_IN_MODIFY(self, event):
1100+ """Capture a modify event and fake an open ^ close write events."""
1101+ # lets ignore dir changes
1102+ if event.dir:
1103+ return
1104+ # on darwin we just get IN_MODIFY, lets always fake
1105+ # an OPEN & CLOSE_WRITE couple
1106+ raw_open = raw_close = {
1107+ 'wd': event.wd,
1108+ 'dir': event.dir,
1109+ 'name': event.name,
1110+ 'path': event.path}
1111+ # calculate the open mask
1112+ raw_open['mask'] = IN_OPEN
1113+ # create the event using the raw data, then fix the pathname param
1114+ open_event = Event(raw_open)
1115+ open_event.pathname = event.pathname
1116+ # push the open
1117+ self.general_processor.push_event(open_event)
1118+ raw_close['mask'] = IN_CLOSE_WRITE
1119+ close_event = Event(raw_close)
1120+ close_event.pathname = event.pathname
1121+ # push the close event
1122+ self.general_processor.push_event(close_event)
1123+
1124+ def process_IN_MOVED_FROM(self, event):
1125+ """Capture the MOVED_FROM to maybe syntethize FILE_MOVED."""
1126+ if self.held_event is not None:
1127+ self.general_processor.log.warn('Lost pair event of %s',
1128+ self.held_event)
1129+ self.held_event = event
1130+
1131+ def _fake_create_event(self, event):
1132+ """Fake the creation of an event."""
1133+ # this is the case of a MOVE from an ignored path (links for example)
1134+ # to a valid path
1135+ if event.dir:
1136+ evtname = "FS_DIR_"
1137+ else:
1138+ evtname = "FS_FILE_"
1139+ self.general_processor.eq_push(evtname + "CREATE", path=event.pathname)
1140+ if not event.dir:
1141+ self.general_processor.eq_push('FS_FILE_CLOSE_WRITE',
1142+ path=event.pathname)
1143+
1144+ def _fake_delete_create_event(self, event):
1145+ """Fake the deletion and the creation."""
1146+ # this is the case of a MOVE from a watch UDF to a diff UDF which
1147+ # means that we have to copy the way linux works.
1148+ if event.dir:
1149+ evtname = "FS_DIR_"
1150+ else:
1151+ evtname = "FS_FILE_"
1152+ m = "Delete because of different shares: %r"
1153+ self.log.info(m, self.held_event.pathname)
1154+ self.general_processor.eq_push(evtname + "DELETE",
1155+ path=self.held_event.pathname)
1156+ self.general_processor.eq_push(evtname + "CREATE", path=event.pathname)
1157+ if not event.dir:
1158+ self.general_processor.eq_push('FS_FILE_CLOSE_WRITE',
1159+ path=event.pathname)
1160+
1161+ def process_IN_MOVED_TO(self, event):
1162+ """Capture the MOVED_TO to maybe syntethize FILE_MOVED."""
1163+ if self.held_event is not None:
1164+ if event.cookie == self.held_event.cookie:
1165+ f_path_dir = os.path.split(self.held_event.pathname)[0]
1166+ t_path_dir = os.path.split(event.pathname)[0]
1167+
1168+ is_from_forreal = not self.is_ignored(self.held_event.pathname)
1169+ is_to_forreal = not self.is_ignored(event.pathname)
1170+ if is_from_forreal and is_to_forreal:
1171+ f_share_id = self.general_processor.get_path_share_id(
1172+ f_path_dir)
1173+ t_share_id = self.general_processor.get_path_share_id(
1174+ t_path_dir)
1175+ if f_share_id != t_share_id:
1176+ # if the share_id are != push a delete/create
1177+ self._fake_delete_create_event(event)
1178+ else:
1179+ if event.dir:
1180+ evtname = "FS_DIR_"
1181+ else:
1182+ evtname = "FS_FILE_"
1183+ self.general_processor.eq_push(evtname + "MOVE",
1184+ path_from=self.held_event.pathname,
1185+ path_to=event.pathname)
1186+ elif is_to_forreal:
1187+ # this is the case of a MOVE from something ignored
1188+ # to a valid filename
1189+ self._fake_create_event(event)
1190+
1191+ self.held_event = None
1192+ return
1193+ else:
1194+ # we should never get to this point on darwin, never ever!
1195+ self.release_held_event()
1196+ self.general_processor.push_event(event)
1197+ else:
1198+ # we should never get to this point on darwin, never ever!
1199+ self.general_processor.log.warn(
1200+ 'Cookie does not match the previous held event!')
1201+ self.general_processor.log.warn('Ignoring %s', event)
1202+
1203+ def process_default(self, event):
1204+ """Push the event into the EventQueue."""
1205+ if self.held_event is not None:
1206+ self.release_held_event()
1207+ self.general_processor.push_event(event)
1208+
1209+ def handle_dir_delete(self, fullpath):
1210+ """Some special work when a directory is deleted."""
1211+ self.general_processor.rm_watch(fullpath)
1212+
1213+ # handle the case of move a dir to a non-watched directory
1214+ paths = self.general_processor.get_paths_starting_with(fullpath,
1215+ include_base=False)
1216+
1217+ paths.sort(reverse=True)
1218+ for path, is_dir in paths:
1219+ m = "Pushing deletion because of parent dir move: (is_dir=%s) %r"
1220+ self.general_processor.log.info(m, is_dir, path)
1221+ if is_dir:
1222+ # same as the above remove
1223+ self.general_processor.rm_watch(path)
1224+
1225+ self.general_processor.eq_push('FS_DIR_DELETE', path=path)
1226+ else:
1227+ self.general_processor.eq_push('FS_FILE_DELETE', path=path)
1228+
1229+ def freeze_begin(self, path):
1230+ """Puts in hold all the events for this path."""
1231+ self.general_processor.freeze_begin(path)
1232+
1233+ def freeze_rollback(self):
1234+ """Unfreezes the frozen path, reseting to idle state."""
1235+ self.general_processor.freeze_rollback()
1236+
1237+ def freeze_commit(self, events):
1238+ """Unfreezes the frozen path, sending received events if not dirty.
1239+
1240+ If events for that path happened:
1241+ - return True
1242+ else:
1243+ - push the here received events, return False
1244+ """
1245+ return self.general_processor.freeze_commit(events)
1246+
1247+ @property
1248+ def mute_filter(self):
1249+ """Return the mute filter used by the processor."""
1250+ return self.general_processor.filter
1251+
1252+ @property
1253+ def frozen_path(self):
1254+ """Return the frozen path."""
1255+ return self.general_processor.frozen_path
1256+
1257+ @property
1258+ def log(self):
1259+ """Return the logger of the instance."""
1260+ return self.general_processor.log
1261+
1262+
1263+class FilesystemMonitor(object):
1264+ """Implementation that allows to receive events from the sustem."""
1265+
1266+ def __init__(self, eq, fs, ignore_config=None, timeout=1):
1267+ self.log = logging.getLogger('ubuntuone.SyncDaemon.FSMonitor')
1268+ self.log.setLevel(TRACE)
1269+ self.fs = fs
1270+ self.eq = eq
1271+ self._processor = NotifyProcessor(self, ignore_config)
1272+ self._factory = PyInotifyEventsFactory(self._processor)
1273+ self._protocol = None
1274+
1275+ @defer.inlineCallbacks
1276+ def _connect_to_daemon(self):
1277+ """Connect to the daemon so that we can receive events."""
1278+ description = 'unix:path=%s' % DAEMON_SOCKET
1279+ client = endpoints.clientFromString(reactor, description)
1280+ self._protocol = yield client.connect(self._factory)
1281+ # add the user with no paths
1282+ yield self._protocol.add_user([])
1283+
1284+ def add_to_mute_filter(self, event, **info):
1285+ """Add info to mute filter in the processor."""
1286+ self._processor.add_to_mute_filter(event, info)
1287+
1288+ def rm_from_mute_filter(self, event, **info):
1289+ """Remove info to mute filter in the processor."""
1290+ self._processor.rm_from_mute_filter(event, info)
1291+
1292+ def shutdown(self):
1293+ """Prepares the EQ to be closed."""
1294+ if self._protocol is not None:
1295+
1296+ def on_user_removed(data):
1297+ """We managed to remove the user."""
1298+ self._protocol.transport.loseConnection()
1299+ self._protocol = None
1300+ return True
1301+
1302+ def on_user_not_removed(reason):
1303+ """We did not manage to remove the user."""
1304+ return True
1305+
1306+ d = self._protocol.remove_user()
1307+ d.addCallback(on_user_removed)
1308+ d.addErrback(on_user_not_removed)
1309+ return d
1310+ return defer.succeed(True)
1311+
1312+ @defer.inlineCallbacks
1313+ def rm_watch(self, dirpath):
1314+ """Remove watch from a dir."""
1315+ # in mac os x we are only watching the parent watches, this is an
1316+ # important details because we will only send a real remove_path to the
1317+ # daemon if the path is the parent path else we will filter it in the
1318+ # factory level
1319+
1320+ if not dirpath[-1] == os.path.sep:
1321+ dirpath += os.path.sep
1322+
1323+ if dirpath not in self._factory.watched_paths:
1324+ # we are watching a parent path but we are not a root one
1325+ # therefore we are going to add it as an ignored path and
1326+ # return
1327+ self._factory.ignored_paths.append(dirpath)
1328+ defer.returnValue(None)
1329+
1330+ # if we got to this point we want to remove a root dir, this is an
1331+ # important detail to take care of. Connect if needed and tell the
1332+ # daemon to remove the path
1333+ if self._protocol is None:
1334+ # we have not yet connected, lets do it!
1335+ yield self._connect_to_daemon()
1336+ was_removed = yield self._protocol.remove_path(dirpath)
1337+ # only remove it if we really removed it
1338+ if was_removed:
1339+ self._factory.watched_paths.remove(dirpath)
1340+
1341+ @defer.inlineCallbacks
1342+ def add_watch(self, dirpath):
1343+ """Add watch to a dir."""
1344+ if not dirpath[-1] == os.path.sep:
1345+ dirpath = dirpath + os.path.sep
1346+
1347+ # if we are watching a parent dir we can just ensure that it is not ignored
1348+ if any(dirpath.startswith(watched_path) for watched_path in
1349+ self._factory.watched_paths):
1350+ if dirpath in self._factory.ignored_paths:
1351+ self._factory.ignored_paths.remove(dirpath)
1352+ defer.returnValue(True)
1353+
1354+ if dirpath in self._factory.ignored_paths:
1355+ self._factory.ignored_paths.remove(dirpath)
1356+ defer.returnValue(True)
1357+
1358+ if self._protocol is None:
1359+ # we have not yet connected, lets do it!
1360+ yield self._connect_to_daemon()
1361+
1362+ was_added = yield self._protocol.add_path(dirpath)
1363+ if was_added:
1364+ self._factory.watched_paths.append(dirpath)
1365+ defer.returnValue(True)
1366+
1367+ def add_watches_to_udf_ancestors(self, volume):
1368+ """Add a inotify watch to volume's ancestors if it's an UDF."""
1369+ # On Mac OS X we do no need to add watches to the ancestors because we
1370+ # will get the events from them with no problem.
1371+ return defer.succeed(True)
1372+
1373+ def is_frozen(self):
1374+ """Checks if there's something frozen."""
1375+ return self._processor.frozen_path is not None
1376+
1377+ def freeze_begin(self, path):
1378+ """Puts in hold all the events for this path."""
1379+ if self._processor.frozen_path is not None:
1380+ raise ValueError("There's something already frozen!")
1381+ self._processor.freeze_begin(path)
1382+
1383+ def freeze_rollback(self):
1384+ """Unfreezes the frozen path, reseting to idle state."""
1385+ if self._processor.frozen_path is None:
1386+ raise ValueError("Rolling back with nothing frozen!")
1387+ self._processor.freeze_rollback()
1388+
1389+ def freeze_commit(self, events):
1390+ """Unfreezes the frozen path, sending received events if not dirty.
1391+
1392+ If events for that path happened:
1393+ - return True
1394+ else:
1395+ - push the here received events, return False
1396+ """
1397+ if self._processor.frozen_path is None:
1398+ raise ValueError("Committing with nothing frozen!")
1399+
1400+ d = defer.execute(self._processor.freeze_commit, events)
1401+ return d
1402
1403=== modified file 'ubuntuone/syncdaemon/filesystem_manager.py'
1404--- ubuntuone/syncdaemon/filesystem_manager.py 2012-04-09 20:07:05 +0000
1405+++ ubuntuone/syncdaemon/filesystem_manager.py 2012-07-19 14:21:30 +0000
1406@@ -744,6 +744,7 @@
1407 mdobj = self.fs[mdid]
1408 if mdobj["share_id"] == share_id:
1409 all_mdobjs.append(_MDObject(**mdobj))
1410+
1411 return all_mdobjs
1412
1413 def get_mdobjs_in_dir(self, base_path):

Subscribers

People subscribed via source and target branches