Merge lp:~diegosarmentero/ubuntuone-client/darwin-fsevents-1 into lp:ubuntuone-client

Proposed by Diego Sarmentero
Status: Merged
Approved by: Manuel de la Peña
Approved revision: 1260
Merged at revision: 1264
Proposed branch: lp:~diegosarmentero/ubuntuone-client/darwin-fsevents-1
Merge into: lp:ubuntuone-client
Diff against target: 1298 lines (+589/-602)
6 files modified
contrib/testing/testcase.py (+7/-3)
tests/platform/filesystem_notifications/__init__.py (+55/-0)
tests/platform/filesystem_notifications/test_linux.py (+5/-53)
tests/platform/filesystem_notifications/test_pyinotify_agnostic.py (+4/-1)
tests/platform/filesystem_notifier/__init__.py (+0/-27)
ubuntuone/platform/filesystem_notifications/pyinotify_agnostic.py (+518/-518)
To merge this branch: bzr merge lp:~diegosarmentero/ubuntuone-client/darwin-fsevents-1
Reviewer Review Type Date Requested Status
Manuel de la Peña (community) Approve
Alejandro J. Cura (community) Approve
Review via email: mp+110382@code.launchpad.net

Commit message

- Some refactoring to support mac os filesystem notifications in the future (LP: #1013323).

To post a comment you must log in.
1259. By Diego Sarmentero

Fixing encoding problem on pyinotify_agnostic

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

The only real changes for pyinotify_agnostic are:
- Adding import sys

and:

805def simple(self, s, attribute):
806 + if isinstance(s, unicode):
807 + s = s.encode(sys.getfilesystemencoding(), 'replace')

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Nice branch so far, here's a small comment:
----
The decorators like: "skip_if_darwin_and_uses_metadata_older_than_5"
Instead of having a pair of decorators for both darwin and windows, I think they should be merged so they are called something like "skip_if_not_linux_and_uses..."

review: Needs Fixing
Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

> Nice branch so far, here's a small comment:
> ----
> The decorators like: "skip_if_darwin_and_uses_metadata_older_than_5"
> Instead of having a pair of decorators for both darwin and windows, I think
> they should be merged so they are called something like
> "skip_if_not_linux_and_uses..."

Fixed

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

Branch reverted, because we really need the two different skips for different situations.

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Here are some more comments that I didn't think of on my first review:

----

This first decorator makes sense; I'm not sure about the other two.
Anyway, the string in the first one is wrong:

+skip_if_darwin_and_uses_metadata_older_than_5 = \
+ skipIfOS('darwin',
+ 'In windows there is no need to migrate metadata older than v5.')

Do we know of any Read/Only issue on darwin to justify this decorator?

+skip_if_darwin_and_uses_readonly = \
+ skipIfOS('darwin', 'Can not test RO shares until bug #820350 is resolved.')

We may have some missing or out of order events in darwin, but we surely need to apply this decorator to the specific tests that fail with darwin, and we have to make sure we don't do a blanket decoration of the same test cases than in windows. In any case, we need a new bug with the darwin specifics.

+skip_if_darwin_missing_fs_event = \
+ skipIfOS('darwin', 'Fails due to missing/out of order FS events, '
+ 'see bug #820598.')

1260. By Diego Sarmentero

branch fixed according comments in the mp

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

> Here are some more comments that I didn't think of on my first review:
>
> ----
>
> This first decorator makes sense; I'm not sure about the other two.
> Anyway, the string in the first one is wrong:
>
> +skip_if_darwin_and_uses_metadata_older_than_5 = \
> + skipIfOS('darwin',
> + 'In windows there is no need to migrate metadata older than
> v5.')
>
>
> Do we know of any Read/Only issue on darwin to justify this decorator?
>
> +skip_if_darwin_and_uses_readonly = \
> + skipIfOS('darwin', 'Can not test RO shares until bug #820350 is
> resolved.')
>
>
> We may have some missing or out of order events in darwin, but we surely need
> to apply this decorator to the specific tests that fail with darwin, and we
> have to make sure we don't do a blanket decoration of the same test cases than
> in windows. In any case, we need a new bug with the darwin specifics.
>
> +skip_if_darwin_missing_fs_event = \
> + skipIfOS('darwin', 'Fails due to missing/out of order FS events, '
> + 'see bug #820598.')

I removed the decorators that weren't being used, and leave "skip_if_darwin_missing_fs_event", because we are using it for some tests in futures branches of the darwin implementation.

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Looks good so far. +1

review: Approve
Revision history for this message
Manuel de la Peña (mandel) wrote :

Looks good, this will be nice to merge with my work so far.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'contrib/testing/testcase.py'
2--- contrib/testing/testcase.py 2012-05-22 14:07:55 +0000
3+++ contrib/testing/testcase.py 2012-06-21 17:37:46 +0000
4@@ -197,7 +197,7 @@
5
6 cancel_download = cancel_upload = download = upload = make_dir = disconnect
7 make_file = move = unlink = list_shares = disconnect
8- list_volumes = create_share = create_udf = inquire_free_space = disconnect
9+ list_volumes = create_share = create_udf = inquire_free_space = disconnect
10 inquire_account_info = delete_volume = change_public_access = disconnect
11 query_volumes = get_delta = rescan_from_scratch = delete_share = disconnect
12 node_is_with_queued_move = cleanup = get_public_files = disconnect
13@@ -302,7 +302,7 @@
14 mmtree(path): support read-only shares
15 makedirs(path): support read-only shares
16 """
17- MAX_FILENAME = 32 # some platforms limit lengths of filenames
18+ MAX_FILENAME = 32 # some platforms limit lengths of filenames
19 tunnel_runner_class = FakeTunnelRunner
20
21 def mktemp(self, name='temp'):
22@@ -448,7 +448,7 @@
23 def __init__(self, root_path):
24 """ Creates the instance"""
25 self.root = volume_manager.Root(node_id="root_node_id", path=root_path)
26- self.shares = {'':self.root}
27+ self.shares = {'': self.root}
28 self.udfs = {}
29 self.log = logging.getLogger('ubuntuone.SyncDaemon.VM-test')
30
31@@ -686,3 +686,7 @@
32 skip_if_win32_missing_fs_event = \
33 skipIfOS('win32', 'Fails due to missing/out of order FS events, '
34 'see bug #820598.')
35+
36+skip_if_darwin_missing_fs_event = \
37+ skipIfOS('darwin', 'Fails due to missing/out of order FS events, '
38+ 'see bug #820598.')
39
40=== modified file 'tests/platform/filesystem_notifications/__init__.py'
41--- tests/platform/filesystem_notifications/__init__.py 2012-04-30 15:19:03 +0000
42+++ tests/platform/filesystem_notifications/__init__.py 2012-06-21 17:37:46 +0000
43@@ -25,3 +25,58 @@
44 # version. If you delete this exception statement from all source
45 # files in the program, then also delete it here.
46 """Platform/File System Notifications test code."""
47+
48+import logging
49+
50+from twisted.internet import defer
51+
52+from ubuntuone.syncdaemon import (
53+ event_queue,
54+ filesystem_manager,
55+)
56+from contrib.testing import testcase
57+from ubuntuone.devtools.handlers import MementoHandler
58+from ubuntuone.syncdaemon.tritcask import Tritcask
59+
60+
61+class BaseFSMonitorTestCase(testcase.BaseTwistedTestCase):
62+ """Test the structures where we have the path/watch."""
63+
64+ timeout = 3
65+
66+ @defer.inlineCallbacks
67+ def setUp(self):
68+ """Set up."""
69+ yield super(BaseFSMonitorTestCase, self).setUp()
70+ fsmdir = self.mktemp('fsmdir')
71+ partials_dir = self.mktemp('partials_dir')
72+ self.root_dir = self.mktemp('root_dir')
73+ self.vm = testcase.FakeVolumeManager(self.root_dir)
74+ self.tritcask_dir = self.mktemp("tritcask_dir")
75+ self.db = Tritcask(self.tritcask_dir)
76+ self.fs = filesystem_manager.FileSystemManager(fsmdir, partials_dir,
77+ self.vm, self.db)
78+ self.fs.create(path=self.root_dir, share_id='', is_dir=True)
79+ self.fs.set_by_path(path=self.root_dir,
80+ local_hash=None, server_hash=None)
81+ eq = event_queue.EventQueue(self.fs)
82+
83+ self.deferred = deferred = defer.Deferred()
84+
85+ class HitMe(object):
86+ # class-closure, cannot use self, pylint: disable-msg=E0213
87+ def handle_default(innerself, event, **args):
88+ deferred.callback(True)
89+
90+ eq.subscribe(HitMe())
91+ self.monitor = eq.monitor
92+ self.log_handler = MementoHandler()
93+ self.log_handler.setLevel(logging.DEBUG)
94+ self.monitor.log.addHandler(self.log_handler)
95+
96+ @defer.inlineCallbacks
97+ def tearDown(self):
98+ """Clean up the tests."""
99+ self.monitor.shutdown()
100+ self.monitor.log.removeHandler(self.log_handler)
101+ yield super(BaseFSMonitorTestCase, self).tearDown()
102
103=== modified file 'tests/platform/filesystem_notifications/test_linux.py'
104--- tests/platform/filesystem_notifications/test_linux.py 2012-05-23 13:06:42 +0000
105+++ tests/platform/filesystem_notifications/test_linux.py 2012-06-21 17:37:46 +0000
106@@ -30,23 +30,17 @@
107 # files in the program, then also delete it here.
108 """Tests for the Event Queue."""
109
110-import logging
111 import os
112
113 from twisted.internet import defer, reactor
114 from twisted.trial.unittest import TestCase as PlainTestCase
115
116-from ubuntuone.syncdaemon import (
117- event_queue,
118- filesystem_manager,
119-)
120 from contrib.testing import testcase
121-from ubuntuone.devtools.handlers import MementoHandler
122 from ubuntuone.syncdaemon import volume_manager
123-from ubuntuone.syncdaemon.tritcask import Tritcask
124 from ubuntuone.platform.filesystem_notifications import (
125 linux as filesystem_notifications,
126 )
127+from tests.platform.filesystem_notifications import BaseFSMonitorTestCase
128
129 # We normally access to private attribs in tests
130 # pylint: disable=W0212
131@@ -101,50 +95,6 @@
132 self.assertFalse(processor.timer.active())
133
134
135-
136-class BaseFSMonitorTestCase(testcase.BaseTwistedTestCase):
137- """Test the structures where we have the path/watch."""
138-
139- timeout = 3
140-
141- @defer.inlineCallbacks
142- def setUp(self):
143- """Set up."""
144- yield super(BaseFSMonitorTestCase, self).setUp()
145- fsmdir = self.mktemp('fsmdir')
146- partials_dir = self.mktemp('partials_dir')
147- self.root_dir = self.mktemp('root_dir')
148- self.vm = testcase.FakeVolumeManager(self.root_dir)
149- self.tritcask_dir = self.mktemp("tritcask_dir")
150- self.db = Tritcask(self.tritcask_dir)
151- self.fs = filesystem_manager.FileSystemManager(fsmdir, partials_dir,
152- self.vm, self.db)
153- self.fs.create(path=self.root_dir, share_id='', is_dir=True)
154- self.fs.set_by_path(path=self.root_dir,
155- local_hash=None, server_hash=None)
156- eq = event_queue.EventQueue(self.fs)
157-
158- self.deferred = deferred = defer.Deferred()
159-
160- class HitMe(object):
161- # class-closure, cannot use self, pylint: disable-msg=E0213
162- def handle_default(innerself, event, **args):
163- deferred.callback(True)
164-
165- eq.subscribe(HitMe())
166- self.monitor = eq.monitor
167- self.log_handler = MementoHandler()
168- self.log_handler.setLevel(logging.DEBUG)
169- self.monitor.log.addHandler(self.log_handler)
170-
171- @defer.inlineCallbacks
172- def tearDown(self):
173- """Clean up the tests."""
174- self.monitor.shutdown()
175- self.monitor.log.removeHandler(self.log_handler)
176- yield super(BaseFSMonitorTestCase, self).tearDown()
177-
178-
179 class WatchManagerTests(BaseFSMonitorTestCase):
180 """Test the structures where we have the path/watch."""
181
182@@ -222,7 +172,8 @@
183 self.monitor._general_watchs = {'/path1/foo': 1, '/other': 2}
184 self.monitor._ancestors_watchs = {'/foo': 3}
185 self.monitor.inotify_watch_fix('/path1/foo', '/path1/new')
186- self.assertEqual(self.monitor._general_watchs, {'/path1/new': 1, '/other': 2})
187+ self.assertEqual(self.monitor._general_watchs,
188+ {'/path1/new': 1, '/other': 2})
189 self.assertEqual(self.monitor._ancestors_watchs, {'/foo': 3})
190
191 def test_fix_path_ancestors(self):
192@@ -231,7 +182,8 @@
193 self.monitor._ancestors_watchs = {'/oth': 1, '/other': 2}
194 self.monitor.inotify_watch_fix('/oth', '/baz')
195 self.assertEqual(self.monitor._general_watchs, {'/bar': 3})
196- self.assertEqual(self.monitor._ancestors_watchs, {'/baz': 1, '/other': 2})
197+ self.assertEqual(self.monitor._ancestors_watchs,
198+ {'/baz': 1, '/other': 2})
199
200
201 class DynamicHitMe(object):
202
203=== renamed file 'tests/platform/filesystem_notifier/test_windows.py' => 'tests/platform/filesystem_notifications/test_pyinotify_agnostic.py'
204--- tests/platform/filesystem_notifier/test_windows.py 2012-05-14 21:24:24 +0000
205+++ tests/platform/filesystem_notifications/test_pyinotify_agnostic.py 2012-06-21 17:37:46 +0000
206@@ -30,6 +30,8 @@
207 # files in the program, then also delete it here.
208 """Test for the pyinotify implementation on windows."""
209
210+import sys
211+
212 from twisted.internet import defer
213 from twisted.trial.unittest import TestCase
214
215@@ -51,7 +53,8 @@
216 attr = 'attribute'
217 self.format[attr] = attr
218 value = u'ñoño'
219- expected_result = (attr + value.encode('mbcs', 'replace') +
220+ expected_result = (attr + value.encode(
221+ sys.getfilesystemencoding(), 'replace') +
222 self.format['normal'])
223 self.assertEqual(expected_result, self.formatter.simple(value, attr))
224
225
226=== removed directory 'tests/platform/filesystem_notifier'
227=== removed file 'tests/platform/filesystem_notifier/__init__.py'
228--- tests/platform/filesystem_notifier/__init__.py 2012-05-14 19:04:43 +0000
229+++ tests/platform/filesystem_notifier/__init__.py 1970-01-01 00:00:00 +0000
230@@ -1,27 +0,0 @@
231-# Copyright 2012 Canonical Ltd.
232-#
233-# This program is free software: you can redistribute it and/or modify it
234-# under the terms of the GNU General Public License version 3, as published
235-# by the Free Software Foundation.
236-#
237-# This program is distributed in the hope that it will be useful, but
238-# WITHOUT ANY WARRANTY; without even the implied warranties of
239-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
240-# PURPOSE. See the GNU General Public License for more details.
241-#
242-# You should have received a copy of the GNU General Public License along
243-# with this program. If not, see <http://www.gnu.org/licenses/>.
244-#
245-# In addition, as a special exception, the copyright holders give
246-# permission to link the code of portions of this program with the
247-# OpenSSL library under certain conditions as described in each
248-# individual source file, and distribute linked combinations
249-# including the two.
250-# You must obey the GNU General Public License in all respects
251-# for all of the code used other than OpenSSL. If you modify
252-# file(s) with this exception, you may extend this exception to your
253-# version of the file(s), but you are not obligated to do so. If you
254-# do not wish to do so, delete this exception statement from your
255-# version. If you delete this exception statement from all source
256-# files in the program, then also delete it here.
257-"""Platform/File System Notifier (Pyinotify agnostic) test code."""
258
259=== modified file 'ubuntuone/platform/filesystem_notifications/pyinotify_agnostic.py'
260--- ubuntuone/platform/filesystem_notifications/pyinotify_agnostic.py 2012-05-14 20:38:23 +0000
261+++ ubuntuone/platform/filesystem_notifications/pyinotify_agnostic.py 2012-06-21 17:37:46 +0000
262@@ -1,518 +1,518 @@
263-#!/usr/bin/env python
264-
265-# pyinotify.py - python interface to inotify
266-# Copyright (c) 2010 Sebastien Martini <seb@dbzteam.org>
267-#
268-# Permission is hereby granted, free of charge, to any person obtaining a copy
269-# of this software and associated documentation files (the "Software"), to deal
270-# in the Software without restriction, including without limitation the rights
271-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
272-# copies of the Software, and to permit persons to whom the Software is
273-# furnished to do so, subject to the following conditions:
274-#
275-# The above copyright notice and this permission notice shall be included in
276-# all copies or substantial portions of the Software.
277-#
278-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
279-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
280-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
281-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
282-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
283-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
284-# THE SOFTWARE.
285-"""Platform agnostic code grabed from pyinotify."""
286-import logging
287-import os
288-import sys
289-
290-COMPATIBILITY_MODE = False
291-
292-class PyinotifyError(Exception):
293- """Indicates exceptions raised by a Pyinotify class."""
294- pass
295-
296-
297-class RawOutputFormat:
298- """
299- Format string representations.
300- """
301- def __init__(self, format=None):
302- self.format = format or {}
303-
304- def simple(self, s, attribute):
305- if isinstance(s, unicode):
306- s = s.encode('mbcs', 'replace')
307- else:
308- s = str(s)
309- return (self.format.get(attribute, '') + s +
310- self.format.get('normal', ''))
311-
312- def punctuation(self, s):
313- """Punctuation color."""
314- return self.simple(s, 'normal')
315-
316- def field_value(self, s):
317- """Field value color."""
318- return self.simple(s, 'purple')
319-
320- def field_name(self, s):
321- """Field name color."""
322- return self.simple(s, 'blue')
323-
324- def class_name(self, s):
325- """Class name color."""
326- return self.format.get('red', '') + self.simple(s, 'bold')
327-
328-output_format = RawOutputFormat()
329-
330-
331-class EventsCodes:
332- """
333- Set of codes corresponding to each kind of events.
334- Some of these flags are used to communicate with inotify, whereas
335- the others are sent to userspace by inotify notifying some events.
336-
337- @cvar IN_ACCESS: File was accessed.
338- @type IN_ACCESS: int
339- @cvar IN_MODIFY: File was modified.
340- @type IN_MODIFY: int
341- @cvar IN_ATTRIB: Metadata changed.
342- @type IN_ATTRIB: int
343- @cvar IN_CLOSE_WRITE: Writtable file was closed.
344- @type IN_CLOSE_WRITE: int
345- @cvar IN_CLOSE_NOWRITE: Unwrittable file closed.
346- @type IN_CLOSE_NOWRITE: int
347- @cvar IN_OPEN: File was opened.
348- @type IN_OPEN: int
349- @cvar IN_MOVED_FROM: File was moved from X.
350- @type IN_MOVED_FROM: int
351- @cvar IN_MOVED_TO: File was moved to Y.
352- @type IN_MOVED_TO: int
353- @cvar IN_CREATE: Subfile was created.
354- @type IN_CREATE: int
355- @cvar IN_DELETE: Subfile was deleted.
356- @type IN_DELETE: int
357- @cvar IN_DELETE_SELF: Self (watched item itself) was deleted.
358- @type IN_DELETE_SELF: int
359- @cvar IN_MOVE_SELF: Self (watched item itself) was moved.
360- @type IN_MOVE_SELF: int
361- @cvar IN_UNMOUNT: Backing fs was unmounted.
362- @type IN_UNMOUNT: int
363- @cvar IN_Q_OVERFLOW: Event queued overflowed.
364- @type IN_Q_OVERFLOW: int
365- @cvar IN_IGNORED: File was ignored.
366- @type IN_IGNORED: int
367- @cvar IN_ONLYDIR: only watch the path if it is a directory (new
368- in kernel 2.6.15).
369- @type IN_ONLYDIR: int
370- @cvar IN_DONT_FOLLOW: don't follow a symlink (new in kernel 2.6.15).
371- IN_ONLYDIR we can make sure that we don't watch
372- the target of symlinks.
373- @type IN_DONT_FOLLOW: int
374- @cvar IN_MASK_ADD: add to the mask of an already existing watch (new
375- in kernel 2.6.14).
376- @type IN_MASK_ADD: int
377- @cvar IN_ISDIR: Event occurred against dir.
378- @type IN_ISDIR: int
379- @cvar IN_ONESHOT: Only send event once.
380- @type IN_ONESHOT: int
381- @cvar ALL_EVENTS: Alias for considering all of the events.
382- @type ALL_EVENTS: int
383- """
384-
385- # The idea here is 'configuration-as-code' - this way, we get
386- # our nice class constants, but we also get nice human-friendly text
387- # mappings to do lookups against as well, for free:
388- FLAG_COLLECTIONS = {'OP_FLAGS': {
389- 'IN_ACCESS' : 0x00000001, # File was accessed
390- 'IN_MODIFY' : 0x00000002, # File was modified
391- 'IN_ATTRIB' : 0x00000004, # Metadata changed
392- 'IN_CLOSE_WRITE' : 0x00000008, # Writable file was closed
393- 'IN_CLOSE_NOWRITE' : 0x00000010, # Unwritable file closed
394- 'IN_OPEN' : 0x00000020, # File was opened
395- 'IN_MOVED_FROM' : 0x00000040, # File was moved from X
396- 'IN_MOVED_TO' : 0x00000080, # File was moved to Y
397- 'IN_CREATE' : 0x00000100, # Subfile was created
398- 'IN_DELETE' : 0x00000200, # Subfile was deleted
399- 'IN_DELETE_SELF' : 0x00000400, # Self (watched item itself)
400- # was deleted
401- 'IN_MOVE_SELF' : 0x00000800, # Self(watched item itself) was moved
402- },
403- 'EVENT_FLAGS': {
404- 'IN_UNMOUNT' : 0x00002000, # Backing fs was unmounted
405- 'IN_Q_OVERFLOW' : 0x00004000, # Event queued overflowed
406- 'IN_IGNORED' : 0x00008000, # File was ignored
407- },
408- 'SPECIAL_FLAGS': {
409- 'IN_ONLYDIR' : 0x01000000, # only watch the path if it is a
410- # directory
411- 'IN_DONT_FOLLOW' : 0x02000000, # don't follow a symlink
412- 'IN_MASK_ADD' : 0x20000000, # add to the mask of an already
413- # existing watch
414- 'IN_ISDIR' : 0x40000000, # event occurred against dir
415- 'IN_ONESHOT' : 0x80000000, # only send event once
416- },
417- }
418-
419- def maskname(mask):
420- """
421- Returns the event name associated to mask. IN_ISDIR is appended to
422- the result when appropriate. Note: only one event is returned, because
423- only one event can be raised at a given time.
424-
425- @param mask: mask.
426- @type mask: int
427- @return: event name.
428- @rtype: str
429- """
430- ms = mask
431- name = '%s'
432- if mask & IN_ISDIR:
433- ms = mask - IN_ISDIR
434- name = '%s|IN_ISDIR'
435- return name % EventsCodes.ALL_VALUES[ms]
436-
437- maskname = staticmethod(maskname)
438-
439-
440-# So let's now turn the configuration into code
441-EventsCodes.ALL_FLAGS = {}
442-EventsCodes.ALL_VALUES = {}
443-for flagc, valc in EventsCodes.FLAG_COLLECTIONS.items():
444- # Make the collections' members directly accessible through the
445- # class dictionary
446- setattr(EventsCodes, flagc, valc)
447-
448- # Collect all the flags under a common umbrella
449- EventsCodes.ALL_FLAGS.update(valc)
450-
451- # Make the individual masks accessible as 'constants' at globals() scope
452- # and masknames accessible by values.
453- for name, val in valc.items():
454- globals()[name] = val
455- EventsCodes.ALL_VALUES[val] = name
456-
457-
458-# all 'normal' events
459-ALL_EVENTS = reduce(lambda x, y: x | y, EventsCodes.OP_FLAGS.values())
460-EventsCodes.ALL_FLAGS['ALL_EVENTS'] = ALL_EVENTS
461-EventsCodes.ALL_VALUES[ALL_EVENTS] = 'ALL_EVENTS'
462-
463-
464-class _Event:
465- """
466- Event structure, represent events raised by the system. This
467- is the base class and should be subclassed.
468-
469- """
470- def __init__(self, dict_):
471- """
472- Attach attributes (contained in dict_) to self.
473-
474- @param dict_: Set of attributes.
475- @type dict_: dictionary
476- """
477- for tpl in dict_.items():
478- setattr(self, *tpl)
479-
480- def __repr__(self):
481- """
482- @return: Generic event string representation.
483- @rtype: str
484- """
485- s = ''
486- for attr, value in sorted(self.__dict__.items(), key=lambda x: x[0]):
487- if attr.startswith('_'):
488- continue
489- if attr == 'mask':
490- value = hex(getattr(self, attr))
491- elif isinstance(value, basestring) and not value:
492- value = "''"
493- s += ' %s%s%s' % (output_format.field_name(attr),
494- output_format.punctuation('='),
495- output_format.field_value(value))
496-
497- s = '%s%s%s %s' % (output_format.punctuation('<'),
498- output_format.class_name(self.__class__.__name__),
499- s,
500- output_format.punctuation('>'))
501- return s
502-
503- def __str__(self):
504- return repr(self)
505-
506-
507-class _RawEvent(_Event):
508- """
509- Raw event, it contains only the informations provided by the system.
510- It doesn't infer anything.
511- """
512- def __init__(self, wd, mask, cookie, name):
513- """
514- @param wd: Watch Descriptor.
515- @type wd: int
516- @param mask: Bitmask of events.
517- @type mask: int
518- @param cookie: Cookie.
519- @type cookie: int
520- @param name: Basename of the file or directory against which the
521- event was raised in case where the watched directory
522- is the parent directory. None if the event was raised
523- on the watched item itself.
524- @type name: string or None
525- """
526- # Use this variable to cache the result of str(self), this object
527- # is immutable.
528- self._str = None
529- # name: remove trailing '\0'
530- d = {'wd': wd,
531- 'mask': mask,
532- 'cookie': cookie,
533- 'name': name.rstrip('\0')}
534- _Event.__init__(self, d)
535- logging.debug(str(self))
536-
537- def __str__(self):
538- if self._str is None:
539- self._str = _Event.__str__(self)
540- return self._str
541-
542-
543-class Event(_Event):
544- """
545- This class contains all the useful informations about the observed
546- event. However, the presence of each field is not guaranteed and
547- depends on the type of event. In effect, some fields are irrelevant
548- for some kind of event (for example 'cookie' is meaningless for
549- IN_CREATE whereas it is mandatory for IN_MOVE_TO).
550-
551- The possible fields are:
552- - wd (int): Watch Descriptor.
553- - mask (int): Mask.
554- - maskname (str): Readable event name.
555- - path (str): path of the file or directory being watched.
556- - name (str): Basename of the file or directory against which the
557- event was raised in case where the watched directory
558- is the parent directory. None if the event was raised
559- on the watched item itself. This field is always provided
560- even if the string is ''.
561- - pathname (str): Concatenation of 'path' and 'name'.
562- - src_pathname (str): Only present for IN_MOVED_TO events and only in
563- the case where IN_MOVED_FROM events are watched too. Holds the
564- source pathname from where pathname was moved from.
565- - cookie (int): Cookie.
566- - dir (bool): True if the event was raised against a directory.
567-
568- """
569- def __init__(self, raw):
570- """
571- Concretely, this is the raw event plus inferred infos.
572- """
573- _Event.__init__(self, raw)
574- self.maskname = EventsCodes.maskname(self.mask)
575- if COMPATIBILITY_MODE:
576- self.event_name = self.maskname
577- try:
578- if self.name:
579- self.pathname = os.path.abspath(os.path.join(self.path,
580- self.name))
581- else:
582- self.pathname = os.path.abspath(self.path)
583- except AttributeError, err:
584- # Usually it is not an error some events are perfectly valids
585- # despite the lack of these attributes.
586- logging.debug(err)
587-
588-
589-class ProcessEventError(PyinotifyError):
590- """
591- ProcessEventError Exception. Raised on ProcessEvent error.
592- """
593- def __init__(self, err):
594- """
595- @param err: Exception error description.
596- @type err: string
597- """
598- PyinotifyError.__init__(self, err)
599-
600-
601-class _ProcessEvent:
602- """
603- Abstract processing event class.
604- """
605- def __call__(self, event):
606- """
607- To behave like a functor the object must be callable.
608- This method is a dispatch method. Its lookup order is:
609- 1. process_MASKNAME method
610- 2. process_FAMILY_NAME method
611- 3. otherwise calls process_default
612-
613- @param event: Event to be processed.
614- @type event: Event object
615- @return: By convention when used from the ProcessEvent class:
616- - Returning False or None (default value) means keep on
617- executing next chained functors (see chain.py example).
618- - Returning True instead means do not execute next
619- processing functions.
620- @rtype: bool
621- @raise ProcessEventError: Event object undispatchable,
622- unknown event.
623- """
624- stripped_mask = event.mask - (event.mask & IN_ISDIR)
625- maskname = EventsCodes.ALL_VALUES.get(stripped_mask)
626- if maskname is None:
627- raise ProcessEventError("Unknown mask 0x%08x" % stripped_mask)
628-
629- # 1- look for process_MASKNAME
630- meth = getattr(self, 'process_' + maskname, None)
631- if meth is not None:
632- return meth(event)
633- # 2- look for process_FAMILY_NAME
634- meth = getattr(self, 'process_IN_' + maskname.split('_')[1], None)
635- if meth is not None:
636- return meth(event)
637- # 3- default call method process_default
638- return self.process_default(event)
639-
640- def __repr__(self):
641- return '<%s>' % self.__class__.__name__
642-
643-
644-class ProcessEvent(_ProcessEvent):
645- """
646- Process events objects, can be specialized via subclassing, thus its
647- behavior can be overriden:
648-
649- Note: you should not override __init__ in your subclass instead define
650- a my_init() method, this method will be called automatically from the
651- constructor of this class with its optionals parameters.
652-
653- 1. Provide specialized individual methods, e.g. process_IN_DELETE for
654- processing a precise type of event (e.g. IN_DELETE in this case).
655- 2. Or/and provide methods for processing events by 'family', e.g.
656- process_IN_CLOSE method will process both IN_CLOSE_WRITE and
657- IN_CLOSE_NOWRITE events (if process_IN_CLOSE_WRITE and
658- process_IN_CLOSE_NOWRITE aren't defined though).
659- 3. Or/and override process_default for catching and processing all
660- the remaining types of events.
661- """
662- pevent = None
663-
664- def __init__(self, pevent=None, **kargs):
665- """
666- Enable chaining of ProcessEvent instances.
667-
668- @param pevent: Optional callable object, will be called on event
669- processing (before self).
670- @type pevent: callable
671- @param kargs: This constructor is implemented as a template method
672- delegating its optionals keyworded arguments to the
673- method my_init().
674- @type kargs: dict
675- """
676- self.pevent = pevent
677- self.my_init(**kargs)
678-
679- def my_init(self, **kargs):
680- """
681- This method is called from ProcessEvent.__init__(). This method is
682- empty here and must be redefined to be useful. In effect, if you
683- need to specifically initialize your subclass' instance then you
684- just have to override this method in your subclass. Then all the
685- keyworded arguments passed to ProcessEvent.__init__() will be
686- transmitted as parameters to this method. Beware you MUST pass
687- keyword arguments though.
688-
689- @param kargs: optional delegated arguments from __init__().
690- @type kargs: dict
691- """
692- pass
693-
694- def __call__(self, event):
695- stop_chaining = False
696- if self.pevent is not None:
697- # By default methods return None so we set as guideline
698- # that methods asking for stop chaining must explicitely
699- # return non None or non False values, otherwise the default
700- # behavior will be to accept chain call to the corresponding
701- # local method.
702- stop_chaining = self.pevent(event)
703- if not stop_chaining:
704- return _ProcessEvent.__call__(self, event)
705-
706- def nested_pevent(self):
707- return self.pevent
708-
709- def process_IN_Q_OVERFLOW(self, event):
710- """
711- By default this method only reports warning messages, you can
712- overredide it by subclassing ProcessEvent and implement your own
713- process_IN_Q_OVERFLOW method. The actions you can take on receiving
714- this event is either to update the variable max_queued_events in order
715- to handle more simultaneous events or to modify your code in order to
716- accomplish a better filtering diminishing the number of raised events.
717- Because this method is defined, IN_Q_OVERFLOW will never get
718- transmitted as arguments to process_default calls.
719-
720- @param event: IN_Q_OVERFLOW event.
721- @type event: dict
722- """
723- logging.warning('Event queue overflowed.')
724-
725- def process_default(self, event):
726- """
727- Default processing event method. By default does nothing. Subclass
728- ProcessEvent and redefine this method in order to modify its behavior.
729-
730- @param event: Event to be processed. Can be of any type of events but
731- IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW).
732- @type event: Event instance
733- """
734- pass
735-
736-
737-class PrintAllEvents(ProcessEvent):
738- """
739- Dummy class used to print events strings representations. For instance this
740- class is used from command line to print all received events to stdout.
741- """
742- def my_init(self, out=None):
743- """
744- @param out: Where events will be written.
745- @type out: Object providing a valid file object interface.
746- """
747- if out is None:
748- out = sys.stdout
749- self._out = out
750-
751- def process_default(self, event):
752- """
753- Writes event string representation to file object provided to
754- my_init().
755-
756- @param event: Event to be processed. Can be of any type of events but
757- IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW).
758- @type event: Event instance
759- """
760- self._out.write(str(event))
761- self._out.write('\n')
762- self._out.flush()
763-
764-
765-class WatchManagerError(Exception):
766- """
767- WatchManager Exception. Raised on error encountered on watches
768- operations.
769-
770- """
771- def __init__(self, msg, wmd):
772- """
773- @param msg: Exception string's description.
774- @type msg: string
775- @param wmd: This dictionary contains the wd assigned to paths of the
776- same call for which watches were successfully added.
777- @type wmd: dict
778- """
779- self.wmd = wmd
780- Exception.__init__(self, msg)
781+#!/usr/bin/env python
782+
783+# pyinotify.py - python interface to inotify
784+# Copyright (c) 2010 Sebastien Martini <seb@dbzteam.org>
785+#
786+# Permission is hereby granted, free of charge, to any person obtaining a copy
787+# of this software and associated documentation files (the "Software"), to deal
788+# in the Software without restriction, including without limitation the rights
789+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
790+# copies of the Software, and to permit persons to whom the Software is
791+# furnished to do so, subject to the following conditions:
792+#
793+# The above copyright notice and this permission notice shall be included in
794+# all copies or substantial portions of the Software.
795+#
796+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
797+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
798+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
799+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
800+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
801+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
802+# THE SOFTWARE.
803+"""Platform agnostic code grabed from pyinotify."""
804+import logging
805+import os
806+import sys
807+
808+COMPATIBILITY_MODE = False
809+
810+class PyinotifyError(Exception):
811+ """Indicates exceptions raised by a Pyinotify class."""
812+ pass
813+
814+
815+class RawOutputFormat:
816+ """
817+ Format string representations.
818+ """
819+ def __init__(self, format=None):
820+ self.format = format or {}
821+
822+ def simple(self, s, attribute):
823+ if isinstance(s, unicode):
824+ s = s.encode(sys.getfilesystemencoding(), 'replace')
825+ else:
826+ s = str(s)
827+ return (self.format.get(attribute, '') + s +
828+ self.format.get('normal', ''))
829+
830+ def punctuation(self, s):
831+ """Punctuation color."""
832+ return self.simple(s, 'normal')
833+
834+ def field_value(self, s):
835+ """Field value color."""
836+ return self.simple(s, 'purple')
837+
838+ def field_name(self, s):
839+ """Field name color."""
840+ return self.simple(s, 'blue')
841+
842+ def class_name(self, s):
843+ """Class name color."""
844+ return self.format.get('red', '') + self.simple(s, 'bold')
845+
846+output_format = RawOutputFormat()
847+
848+
849+class EventsCodes:
850+ """
851+ Set of codes corresponding to each kind of events.
852+ Some of these flags are used to communicate with inotify, whereas
853+ the others are sent to userspace by inotify notifying some events.
854+
855+ @cvar IN_ACCESS: File was accessed.
856+ @type IN_ACCESS: int
857+ @cvar IN_MODIFY: File was modified.
858+ @type IN_MODIFY: int
859+ @cvar IN_ATTRIB: Metadata changed.
860+ @type IN_ATTRIB: int
861+ @cvar IN_CLOSE_WRITE: Writtable file was closed.
862+ @type IN_CLOSE_WRITE: int
863+ @cvar IN_CLOSE_NOWRITE: Unwrittable file closed.
864+ @type IN_CLOSE_NOWRITE: int
865+ @cvar IN_OPEN: File was opened.
866+ @type IN_OPEN: int
867+ @cvar IN_MOVED_FROM: File was moved from X.
868+ @type IN_MOVED_FROM: int
869+ @cvar IN_MOVED_TO: File was moved to Y.
870+ @type IN_MOVED_TO: int
871+ @cvar IN_CREATE: Subfile was created.
872+ @type IN_CREATE: int
873+ @cvar IN_DELETE: Subfile was deleted.
874+ @type IN_DELETE: int
875+ @cvar IN_DELETE_SELF: Self (watched item itself) was deleted.
876+ @type IN_DELETE_SELF: int
877+ @cvar IN_MOVE_SELF: Self (watched item itself) was moved.
878+ @type IN_MOVE_SELF: int
879+ @cvar IN_UNMOUNT: Backing fs was unmounted.
880+ @type IN_UNMOUNT: int
881+ @cvar IN_Q_OVERFLOW: Event queued overflowed.
882+ @type IN_Q_OVERFLOW: int
883+ @cvar IN_IGNORED: File was ignored.
884+ @type IN_IGNORED: int
885+ @cvar IN_ONLYDIR: only watch the path if it is a directory (new
886+ in kernel 2.6.15).
887+ @type IN_ONLYDIR: int
888+ @cvar IN_DONT_FOLLOW: don't follow a symlink (new in kernel 2.6.15).
889+ IN_ONLYDIR we can make sure that we don't watch
890+ the target of symlinks.
891+ @type IN_DONT_FOLLOW: int
892+ @cvar IN_MASK_ADD: add to the mask of an already existing watch (new
893+ in kernel 2.6.14).
894+ @type IN_MASK_ADD: int
895+ @cvar IN_ISDIR: Event occurred against dir.
896+ @type IN_ISDIR: int
897+ @cvar IN_ONESHOT: Only send event once.
898+ @type IN_ONESHOT: int
899+ @cvar ALL_EVENTS: Alias for considering all of the events.
900+ @type ALL_EVENTS: int
901+ """
902+
903+ # The idea here is 'configuration-as-code' - this way, we get
904+ # our nice class constants, but we also get nice human-friendly text
905+ # mappings to do lookups against as well, for free:
906+ FLAG_COLLECTIONS = {'OP_FLAGS': {
907+ 'IN_ACCESS' : 0x00000001, # File was accessed
908+ 'IN_MODIFY' : 0x00000002, # File was modified
909+ 'IN_ATTRIB' : 0x00000004, # Metadata changed
910+ 'IN_CLOSE_WRITE' : 0x00000008, # Writable file was closed
911+ 'IN_CLOSE_NOWRITE' : 0x00000010, # Unwritable file closed
912+ 'IN_OPEN' : 0x00000020, # File was opened
913+ 'IN_MOVED_FROM' : 0x00000040, # File was moved from X
914+ 'IN_MOVED_TO' : 0x00000080, # File was moved to Y
915+ 'IN_CREATE' : 0x00000100, # Subfile was created
916+ 'IN_DELETE' : 0x00000200, # Subfile was deleted
917+ 'IN_DELETE_SELF' : 0x00000400, # Self (watched item itself)
918+ # was deleted
919+ 'IN_MOVE_SELF' : 0x00000800, # Self(watched item itself) was moved
920+ },
921+ 'EVENT_FLAGS': {
922+ 'IN_UNMOUNT' : 0x00002000, # Backing fs was unmounted
923+ 'IN_Q_OVERFLOW' : 0x00004000, # Event queued overflowed
924+ 'IN_IGNORED' : 0x00008000, # File was ignored
925+ },
926+ 'SPECIAL_FLAGS': {
927+ 'IN_ONLYDIR' : 0x01000000, # only watch the path if it is a
928+ # directory
929+ 'IN_DONT_FOLLOW' : 0x02000000, # don't follow a symlink
930+ 'IN_MASK_ADD' : 0x20000000, # add to the mask of an already
931+ # existing watch
932+ 'IN_ISDIR' : 0x40000000, # event occurred against dir
933+ 'IN_ONESHOT' : 0x80000000, # only send event once
934+ },
935+ }
936+
937+ def maskname(mask):
938+ """
939+ Returns the event name associated to mask. IN_ISDIR is appended to
940+ the result when appropriate. Note: only one event is returned, because
941+ only one event can be raised at a given time.
942+
943+ @param mask: mask.
944+ @type mask: int
945+ @return: event name.
946+ @rtype: str
947+ """
948+ ms = mask
949+ name = '%s'
950+ if mask & IN_ISDIR:
951+ ms = mask - IN_ISDIR
952+ name = '%s|IN_ISDIR'
953+ return name % EventsCodes.ALL_VALUES[ms]
954+
955+ maskname = staticmethod(maskname)
956+
957+
958+# So let's now turn the configuration into code
959+EventsCodes.ALL_FLAGS = {}
960+EventsCodes.ALL_VALUES = {}
961+for flagc, valc in EventsCodes.FLAG_COLLECTIONS.items():
962+ # Make the collections' members directly accessible through the
963+ # class dictionary
964+ setattr(EventsCodes, flagc, valc)
965+
966+ # Collect all the flags under a common umbrella
967+ EventsCodes.ALL_FLAGS.update(valc)
968+
969+ # Make the individual masks accessible as 'constants' at globals() scope
970+ # and masknames accessible by values.
971+ for name, val in valc.items():
972+ globals()[name] = val
973+ EventsCodes.ALL_VALUES[val] = name
974+
975+
976+# all 'normal' events
977+ALL_EVENTS = reduce(lambda x, y: x | y, EventsCodes.OP_FLAGS.values())
978+EventsCodes.ALL_FLAGS['ALL_EVENTS'] = ALL_EVENTS
979+EventsCodes.ALL_VALUES[ALL_EVENTS] = 'ALL_EVENTS'
980+
981+
982+class _Event:
983+ """
984+ Event structure, represent events raised by the system. This
985+ is the base class and should be subclassed.
986+
987+ """
988+ def __init__(self, dict_):
989+ """
990+ Attach attributes (contained in dict_) to self.
991+
992+ @param dict_: Set of attributes.
993+ @type dict_: dictionary
994+ """
995+ for tpl in dict_.items():
996+ setattr(self, *tpl)
997+
998+ def __repr__(self):
999+ """
1000+ @return: Generic event string representation.
1001+ @rtype: str
1002+ """
1003+ s = ''
1004+ for attr, value in sorted(self.__dict__.items(), key=lambda x: x[0]):
1005+ if attr.startswith('_'):
1006+ continue
1007+ if attr == 'mask':
1008+ value = hex(getattr(self, attr))
1009+ elif isinstance(value, basestring) and not value:
1010+ value = "''"
1011+ s += ' %s%s%s' % (output_format.field_name(attr),
1012+ output_format.punctuation('='),
1013+ output_format.field_value(value))
1014+
1015+ s = '%s%s%s %s' % (output_format.punctuation('<'),
1016+ output_format.class_name(self.__class__.__name__),
1017+ s,
1018+ output_format.punctuation('>'))
1019+ return s
1020+
1021+ def __str__(self):
1022+ return repr(self)
1023+
1024+
1025+class _RawEvent(_Event):
1026+ """
1027+ Raw event, it contains only the informations provided by the system.
1028+ It doesn't infer anything.
1029+ """
1030+ def __init__(self, wd, mask, cookie, name):
1031+ """
1032+ @param wd: Watch Descriptor.
1033+ @type wd: int
1034+ @param mask: Bitmask of events.
1035+ @type mask: int
1036+ @param cookie: Cookie.
1037+ @type cookie: int
1038+ @param name: Basename of the file or directory against which the
1039+ event was raised in case where the watched directory
1040+ is the parent directory. None if the event was raised
1041+ on the watched item itself.
1042+ @type name: string or None
1043+ """
1044+ # Use this variable to cache the result of str(self), this object
1045+ # is immutable.
1046+ self._str = None
1047+ # name: remove trailing '\0'
1048+ d = {'wd': wd,
1049+ 'mask': mask,
1050+ 'cookie': cookie,
1051+ 'name': name.rstrip('\0')}
1052+ _Event.__init__(self, d)
1053+ logging.debug(str(self))
1054+
1055+ def __str__(self):
1056+ if self._str is None:
1057+ self._str = _Event.__str__(self)
1058+ return self._str
1059+
1060+
1061+class Event(_Event):
1062+ """
1063+ This class contains all the useful informations about the observed
1064+ event. However, the presence of each field is not guaranteed and
1065+ depends on the type of event. In effect, some fields are irrelevant
1066+ for some kind of event (for example 'cookie' is meaningless for
1067+ IN_CREATE whereas it is mandatory for IN_MOVE_TO).
1068+
1069+ The possible fields are:
1070+ - wd (int): Watch Descriptor.
1071+ - mask (int): Mask.
1072+ - maskname (str): Readable event name.
1073+ - path (str): path of the file or directory being watched.
1074+ - name (str): Basename of the file or directory against which the
1075+ event was raised in case where the watched directory
1076+ is the parent directory. None if the event was raised
1077+ on the watched item itself. This field is always provided
1078+ even if the string is ''.
1079+ - pathname (str): Concatenation of 'path' and 'name'.
1080+ - src_pathname (str): Only present for IN_MOVED_TO events and only in
1081+ the case where IN_MOVED_FROM events are watched too. Holds the
1082+ source pathname from where pathname was moved from.
1083+ - cookie (int): Cookie.
1084+ - dir (bool): True if the event was raised against a directory.
1085+
1086+ """
1087+ def __init__(self, raw):
1088+ """
1089+ Concretely, this is the raw event plus inferred infos.
1090+ """
1091+ _Event.__init__(self, raw)
1092+ self.maskname = EventsCodes.maskname(self.mask)
1093+ if COMPATIBILITY_MODE:
1094+ self.event_name = self.maskname
1095+ try:
1096+ if self.name:
1097+ self.pathname = os.path.abspath(os.path.join(self.path,
1098+ self.name))
1099+ else:
1100+ self.pathname = os.path.abspath(self.path)
1101+ except AttributeError, err:
1102+ # Usually it is not an error some events are perfectly valids
1103+ # despite the lack of these attributes.
1104+ logging.debug(err)
1105+
1106+
1107+class ProcessEventError(PyinotifyError):
1108+ """
1109+ ProcessEventError Exception. Raised on ProcessEvent error.
1110+ """
1111+ def __init__(self, err):
1112+ """
1113+ @param err: Exception error description.
1114+ @type err: string
1115+ """
1116+ PyinotifyError.__init__(self, err)
1117+
1118+
1119+class _ProcessEvent:
1120+ """
1121+ Abstract processing event class.
1122+ """
1123+ def __call__(self, event):
1124+ """
1125+ To behave like a functor the object must be callable.
1126+ This method is a dispatch method. Its lookup order is:
1127+ 1. process_MASKNAME method
1128+ 2. process_FAMILY_NAME method
1129+ 3. otherwise calls process_default
1130+
1131+ @param event: Event to be processed.
1132+ @type event: Event object
1133+ @return: By convention when used from the ProcessEvent class:
1134+ - Returning False or None (default value) means keep on
1135+ executing next chained functors (see chain.py example).
1136+ - Returning True instead means do not execute next
1137+ processing functions.
1138+ @rtype: bool
1139+ @raise ProcessEventError: Event object undispatchable,
1140+ unknown event.
1141+ """
1142+ stripped_mask = event.mask - (event.mask & IN_ISDIR)
1143+ maskname = EventsCodes.ALL_VALUES.get(stripped_mask)
1144+ if maskname is None:
1145+ raise ProcessEventError("Unknown mask 0x%08x" % stripped_mask)
1146+
1147+ # 1- look for process_MASKNAME
1148+ meth = getattr(self, 'process_' + maskname, None)
1149+ if meth is not None:
1150+ return meth(event)
1151+ # 2- look for process_FAMILY_NAME
1152+ meth = getattr(self, 'process_IN_' + maskname.split('_')[1], None)
1153+ if meth is not None:
1154+ return meth(event)
1155+ # 3- default call method process_default
1156+ return self.process_default(event)
1157+
1158+ def __repr__(self):
1159+ return '<%s>' % self.__class__.__name__
1160+
1161+
1162+class ProcessEvent(_ProcessEvent):
1163+ """
1164+ Process events objects, can be specialized via subclassing, thus its
1165+ behavior can be overriden:
1166+
1167+ Note: you should not override __init__ in your subclass instead define
1168+ a my_init() method, this method will be called automatically from the
1169+ constructor of this class with its optionals parameters.
1170+
1171+ 1. Provide specialized individual methods, e.g. process_IN_DELETE for
1172+ processing a precise type of event (e.g. IN_DELETE in this case).
1173+ 2. Or/and provide methods for processing events by 'family', e.g.
1174+ process_IN_CLOSE method will process both IN_CLOSE_WRITE and
1175+ IN_CLOSE_NOWRITE events (if process_IN_CLOSE_WRITE and
1176+ process_IN_CLOSE_NOWRITE aren't defined though).
1177+ 3. Or/and override process_default for catching and processing all
1178+ the remaining types of events.
1179+ """
1180+ pevent = None
1181+
1182+ def __init__(self, pevent=None, **kargs):
1183+ """
1184+ Enable chaining of ProcessEvent instances.
1185+
1186+ @param pevent: Optional callable object, will be called on event
1187+ processing (before self).
1188+ @type pevent: callable
1189+ @param kargs: This constructor is implemented as a template method
1190+ delegating its optionals keyworded arguments to the
1191+ method my_init().
1192+ @type kargs: dict
1193+ """
1194+ self.pevent = pevent
1195+ self.my_init(**kargs)
1196+
1197+ def my_init(self, **kargs):
1198+ """
1199+ This method is called from ProcessEvent.__init__(). This method is
1200+ empty here and must be redefined to be useful. In effect, if you
1201+ need to specifically initialize your subclass' instance then you
1202+ just have to override this method in your subclass. Then all the
1203+ keyworded arguments passed to ProcessEvent.__init__() will be
1204+ transmitted as parameters to this method. Beware you MUST pass
1205+ keyword arguments though.
1206+
1207+ @param kargs: optional delegated arguments from __init__().
1208+ @type kargs: dict
1209+ """
1210+ pass
1211+
1212+ def __call__(self, event):
1213+ stop_chaining = False
1214+ if self.pevent is not None:
1215+ # By default methods return None so we set as guideline
1216+ # that methods asking for stop chaining must explicitely
1217+ # return non None or non False values, otherwise the default
1218+ # behavior will be to accept chain call to the corresponding
1219+ # local method.
1220+ stop_chaining = self.pevent(event)
1221+ if not stop_chaining:
1222+ return _ProcessEvent.__call__(self, event)
1223+
1224+ def nested_pevent(self):
1225+ return self.pevent
1226+
1227+ def process_IN_Q_OVERFLOW(self, event):
1228+ """
1229+ By default this method only reports warning messages, you can
1230+ overredide it by subclassing ProcessEvent and implement your own
1231+ process_IN_Q_OVERFLOW method. The actions you can take on receiving
1232+ this event is either to update the variable max_queued_events in order
1233+ to handle more simultaneous events or to modify your code in order to
1234+ accomplish a better filtering diminishing the number of raised events.
1235+ Because this method is defined, IN_Q_OVERFLOW will never get
1236+ transmitted as arguments to process_default calls.
1237+
1238+ @param event: IN_Q_OVERFLOW event.
1239+ @type event: dict
1240+ """
1241+ logging.warning('Event queue overflowed.')
1242+
1243+ def process_default(self, event):
1244+ """
1245+ Default processing event method. By default does nothing. Subclass
1246+ ProcessEvent and redefine this method in order to modify its behavior.
1247+
1248+ @param event: Event to be processed. Can be of any type of events but
1249+ IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW).
1250+ @type event: Event instance
1251+ """
1252+ pass
1253+
1254+
1255+class PrintAllEvents(ProcessEvent):
1256+ """
1257+ Dummy class used to print events strings representations. For instance this
1258+ class is used from command line to print all received events to stdout.
1259+ """
1260+ def my_init(self, out=None):
1261+ """
1262+ @param out: Where events will be written.
1263+ @type out: Object providing a valid file object interface.
1264+ """
1265+ if out is None:
1266+ out = sys.stdout
1267+ self._out = out
1268+
1269+ def process_default(self, event):
1270+ """
1271+ Writes event string representation to file object provided to
1272+ my_init().
1273+
1274+ @param event: Event to be processed. Can be of any type of events but
1275+ IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW).
1276+ @type event: Event instance
1277+ """
1278+ self._out.write(str(event))
1279+ self._out.write('\n')
1280+ self._out.flush()
1281+
1282+
1283+class WatchManagerError(Exception):
1284+ """
1285+ WatchManager Exception. Raised on error encountered on watches
1286+ operations.
1287+
1288+ """
1289+ def __init__(self, msg, wmd):
1290+ """
1291+ @param msg: Exception string's description.
1292+ @type msg: string
1293+ @param wmd: This dictionary contains the wd assigned to paths of the
1294+ same call for which watches were successfully added.
1295+ @type wmd: dict
1296+ """
1297+ self.wmd = wmd
1298+ Exception.__init__(self, msg)

Subscribers

People subscribed via source and target branches