~ubuntu-branches/ubuntu/oneiric/ubuntuone-client/oneiric

« back to all changes in this revision

Viewing changes to ubuntuone/platform/windows/pyinotify.py

  • Committer: Bazaar Package Importer
  • Author(s): Rodney Dawes
  • Date: 2011-02-23 18:34:09 UTC
  • mfrom: (1.1.45 upstream)
  • Revision ID: james.westby@ubuntu.com-20110223183409-535o7yo165wbjmca
Tags: 1.5.5-0ubuntu1
* New upstream release.
  - Subscribing to a RO share will not download content (LP: #712528)
  - Can't synchronize "~/Ubuntu One Music" (LP: #714976)
  - Syncdaemon needs to show progress in Unity launcher (LP: #702116)
  - Notifications say "your cloud" (LP: #715887)
  - No longer requires python-libproxy
  - Recommend unity and indicator libs by default

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
# pyinotify.py - python interface to inotify
 
4
# Copyright (c) 2010 Sebastien Martini <seb@dbzteam.org>
 
5
#
 
6
# Permission is hereby granted, free of charge, to any person obtaining a copy
 
7
# of this software and associated documentation files (the "Software"), to deal
 
8
# in the Software without restriction, including without limitation the rights
 
9
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 
10
# copies of the Software, and to permit persons to whom the Software is
 
11
# furnished to do so, subject to the following conditions:
 
12
#
 
13
# The above copyright notice and this permission notice shall be included in
 
14
# all copies or substantial portions of the Software.
 
15
#
 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 
17
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 
18
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 
19
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 
20
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 
21
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 
22
# THE SOFTWARE.
 
23
"""Platform agnostic code grabed from pyinotify."""
 
24
import logging
 
25
import os
 
26
import sys
 
27
 
 
28
COMPATIBILITY_MODE = False
 
29
 
 
30
class PyinotifyError(Exception):
 
31
    """Indicates exceptions raised by a Pyinotify class."""
 
32
    pass
 
33
 
 
34
 
 
35
class RawOutputFormat:
 
36
    """
 
37
    Format string representations.
 
38
    """
 
39
    def __init__(self, format=None):
 
40
        self.format = format or {}
 
41
 
 
42
    def simple(self, s, attribute):
 
43
        if not isinstance(s, str):
 
44
            s = str(s)
 
45
        return (self.format.get(attribute, '') + s +
 
46
                self.format.get('normal', ''))
 
47
 
 
48
    def punctuation(self, s):
 
49
        """Punctuation color."""
 
50
        return self.simple(s, 'normal')
 
51
 
 
52
    def field_value(self, s):
 
53
        """Field value color."""
 
54
        return self.simple(s, 'purple')
 
55
 
 
56
    def field_name(self, s):
 
57
        """Field name color."""
 
58
        return self.simple(s, 'blue')
 
59
 
 
60
    def class_name(self, s):
 
61
        """Class name color."""
 
62
        return self.format.get('red', '') + self.simple(s, 'bold')
 
63
 
 
64
output_format = RawOutputFormat()
 
65
 
 
66
 
 
67
class EventsCodes:
 
68
    """
 
69
    Set of codes corresponding to each kind of events.
 
70
    Some of these flags are used to communicate with inotify, whereas
 
71
    the others are sent to userspace by inotify notifying some events.
 
72
 
 
73
    @cvar IN_ACCESS: File was accessed.
 
74
    @type IN_ACCESS: int
 
75
    @cvar IN_MODIFY: File was modified.
 
76
    @type IN_MODIFY: int
 
77
    @cvar IN_ATTRIB: Metadata changed.
 
78
    @type IN_ATTRIB: int
 
79
    @cvar IN_CLOSE_WRITE: Writtable file was closed.
 
80
    @type IN_CLOSE_WRITE: int
 
81
    @cvar IN_CLOSE_NOWRITE: Unwrittable file closed.
 
82
    @type IN_CLOSE_NOWRITE: int
 
83
    @cvar IN_OPEN: File was opened.
 
84
    @type IN_OPEN: int
 
85
    @cvar IN_MOVED_FROM: File was moved from X.
 
86
    @type IN_MOVED_FROM: int
 
87
    @cvar IN_MOVED_TO: File was moved to Y.
 
88
    @type IN_MOVED_TO: int
 
89
    @cvar IN_CREATE: Subfile was created.
 
90
    @type IN_CREATE: int
 
91
    @cvar IN_DELETE: Subfile was deleted.
 
92
    @type IN_DELETE: int
 
93
    @cvar IN_DELETE_SELF: Self (watched item itself) was deleted.
 
94
    @type IN_DELETE_SELF: int
 
95
    @cvar IN_MOVE_SELF: Self (watched item itself) was moved.
 
96
    @type IN_MOVE_SELF: int
 
97
    @cvar IN_UNMOUNT: Backing fs was unmounted.
 
98
    @type IN_UNMOUNT: int
 
99
    @cvar IN_Q_OVERFLOW: Event queued overflowed.
 
100
    @type IN_Q_OVERFLOW: int
 
101
    @cvar IN_IGNORED: File was ignored.
 
102
    @type IN_IGNORED: int
 
103
    @cvar IN_ONLYDIR: only watch the path if it is a directory (new
 
104
                      in kernel 2.6.15).
 
105
    @type IN_ONLYDIR: int
 
106
    @cvar IN_DONT_FOLLOW: don't follow a symlink (new in kernel 2.6.15).
 
107
                          IN_ONLYDIR we can make sure that we don't watch
 
108
                          the target of symlinks.
 
109
    @type IN_DONT_FOLLOW: int
 
110
    @cvar IN_MASK_ADD: add to the mask of an already existing watch (new
 
111
                       in kernel 2.6.14).
 
112
    @type IN_MASK_ADD: int
 
113
    @cvar IN_ISDIR: Event occurred against dir.
 
114
    @type IN_ISDIR: int
 
115
    @cvar IN_ONESHOT: Only send event once.
 
116
    @type IN_ONESHOT: int
 
117
    @cvar ALL_EVENTS: Alias for considering all of the events.
 
118
    @type ALL_EVENTS: int
 
119
    """
 
120
 
 
121
    # The idea here is 'configuration-as-code' - this way, we get
 
122
    # our nice class constants, but we also get nice human-friendly text
 
123
    # mappings to do lookups against as well, for free:
 
124
    FLAG_COLLECTIONS = {'OP_FLAGS': {
 
125
        'IN_ACCESS'        : 0x00000001,  # File was accessed
 
126
        'IN_MODIFY'        : 0x00000002,  # File was modified
 
127
        'IN_ATTRIB'        : 0x00000004,  # Metadata changed
 
128
        'IN_CLOSE_WRITE'   : 0x00000008,  # Writable file was closed
 
129
        'IN_CLOSE_NOWRITE' : 0x00000010,  # Unwritable file closed
 
130
        'IN_OPEN'          : 0x00000020,  # File was opened
 
131
        'IN_MOVED_FROM'    : 0x00000040,  # File was moved from X
 
132
        'IN_MOVED_TO'      : 0x00000080,  # File was moved to Y
 
133
        'IN_CREATE'        : 0x00000100,  # Subfile was created
 
134
        'IN_DELETE'        : 0x00000200,  # Subfile was deleted
 
135
        'IN_DELETE_SELF'   : 0x00000400,  # Self (watched item itself)
 
136
                                          # was deleted
 
137
        'IN_MOVE_SELF'     : 0x00000800,  # Self(watched item itself) was moved
 
138
        },
 
139
                        'EVENT_FLAGS': {
 
140
        'IN_UNMOUNT'       : 0x00002000,  # Backing fs was unmounted
 
141
        'IN_Q_OVERFLOW'    : 0x00004000,  # Event queued overflowed
 
142
        'IN_IGNORED'       : 0x00008000,  # File was ignored
 
143
        },
 
144
                        'SPECIAL_FLAGS': {
 
145
        'IN_ONLYDIR'       : 0x01000000,  # only watch the path if it is a
 
146
                                          # directory
 
147
        'IN_DONT_FOLLOW'   : 0x02000000,  # don't follow a symlink
 
148
        'IN_MASK_ADD'      : 0x20000000,  # add to the mask of an already
 
149
                                          # existing watch
 
150
        'IN_ISDIR'         : 0x40000000,  # event occurred against dir
 
151
        'IN_ONESHOT'       : 0x80000000,  # only send event once
 
152
        },
 
153
                        }
 
154
 
 
155
    def maskname(mask):
 
156
        """
 
157
        Returns the event name associated to mask. IN_ISDIR is appended to
 
158
        the result when appropriate. Note: only one event is returned, because
 
159
        only one event can be raised at a given time.
 
160
 
 
161
        @param mask: mask.
 
162
        @type mask: int
 
163
        @return: event name.
 
164
        @rtype: str
 
165
        """
 
166
        ms = mask
 
167
        name = '%s'
 
168
        if mask & IN_ISDIR:
 
169
            ms = mask - IN_ISDIR
 
170
            name = '%s|IN_ISDIR'
 
171
        return name % EventsCodes.ALL_VALUES[ms]
 
172
 
 
173
    maskname = staticmethod(maskname)
 
174
 
 
175
 
 
176
# So let's now turn the configuration into code
 
177
EventsCodes.ALL_FLAGS = {}
 
178
EventsCodes.ALL_VALUES = {}
 
179
for flagc, valc in EventsCodes.FLAG_COLLECTIONS.items():
 
180
    # Make the collections' members directly accessible through the
 
181
    # class dictionary
 
182
    setattr(EventsCodes, flagc, valc)
 
183
 
 
184
    # Collect all the flags under a common umbrella
 
185
    EventsCodes.ALL_FLAGS.update(valc)
 
186
 
 
187
    # Make the individual masks accessible as 'constants' at globals() scope
 
188
    # and masknames accessible by values.
 
189
    for name, val in valc.items():
 
190
        globals()[name] = val
 
191
        EventsCodes.ALL_VALUES[val] = name
 
192
 
 
193
 
 
194
# all 'normal' events
 
195
ALL_EVENTS = reduce(lambda x, y: x | y, EventsCodes.OP_FLAGS.values())
 
196
EventsCodes.ALL_FLAGS['ALL_EVENTS'] = ALL_EVENTS
 
197
EventsCodes.ALL_VALUES[ALL_EVENTS] = 'ALL_EVENTS'
 
198
 
 
199
 
 
200
class _Event:
 
201
    """
 
202
    Event structure, represent events raised by the system. This
 
203
    is the base class and should be subclassed.
 
204
 
 
205
    """
 
206
    def __init__(self, dict_):
 
207
        """
 
208
        Attach attributes (contained in dict_) to self.
 
209
 
 
210
        @param dict_: Set of attributes.
 
211
        @type dict_: dictionary
 
212
        """
 
213
        for tpl in dict_.items():
 
214
            setattr(self, *tpl)
 
215
 
 
216
    def __repr__(self):
 
217
        """
 
218
        @return: Generic event string representation.
 
219
        @rtype: str
 
220
        """
 
221
        s = ''
 
222
        for attr, value in sorted(self.__dict__.items(), key=lambda x: x[0]):
 
223
            if attr.startswith('_'):
 
224
                continue
 
225
            if attr == 'mask':
 
226
                value = hex(getattr(self, attr))
 
227
            elif isinstance(value, basestring) and not value:
 
228
                value = "''"
 
229
            s += ' %s%s%s' % (output_format.field_name(attr),
 
230
                              output_format.punctuation('='),
 
231
                              output_format.field_value(value))
 
232
 
 
233
        s = '%s%s%s %s' % (output_format.punctuation('<'),
 
234
                           output_format.class_name(self.__class__.__name__),
 
235
                           s,
 
236
                           output_format.punctuation('>'))
 
237
        return s
 
238
 
 
239
    def __str__(self):
 
240
        return repr(self)
 
241
 
 
242
 
 
243
class _RawEvent(_Event):
 
244
    """
 
245
    Raw event, it contains only the informations provided by the system.
 
246
    It doesn't infer anything.
 
247
    """
 
248
    def __init__(self, wd, mask, cookie, name):
 
249
        """
 
250
        @param wd: Watch Descriptor.
 
251
        @type wd: int
 
252
        @param mask: Bitmask of events.
 
253
        @type mask: int
 
254
        @param cookie: Cookie.
 
255
        @type cookie: int
 
256
        @param name: Basename of the file or directory against which the
 
257
                     event was raised in case where the watched directory
 
258
                     is the parent directory. None if the event was raised
 
259
                     on the watched item itself.
 
260
        @type name: string or None
 
261
        """
 
262
        # Use this variable to cache the result of str(self), this object
 
263
        # is immutable.
 
264
        self._str = None
 
265
        # name: remove trailing '\0'
 
266
        d = {'wd': wd,
 
267
             'mask': mask,
 
268
             'cookie': cookie,
 
269
             'name': name.rstrip('\0')}
 
270
        _Event.__init__(self, d)
 
271
        logging.debug(str(self))
 
272
 
 
273
    def __str__(self):
 
274
        if self._str is None:
 
275
            self._str = _Event.__str__(self)
 
276
        return self._str
 
277
 
 
278
 
 
279
class Event(_Event):
 
280
    """
 
281
    This class contains all the useful informations about the observed
 
282
    event. However, the presence of each field is not guaranteed and
 
283
    depends on the type of event. In effect, some fields are irrelevant
 
284
    for some kind of event (for example 'cookie' is meaningless for
 
285
    IN_CREATE whereas it is mandatory for IN_MOVE_TO).
 
286
 
 
287
    The possible fields are:
 
288
      - wd (int): Watch Descriptor.
 
289
      - mask (int): Mask.
 
290
      - maskname (str): Readable event name.
 
291
      - path (str): path of the file or directory being watched.
 
292
      - name (str): Basename of the file or directory against which the
 
293
              event was raised in case where the watched directory
 
294
              is the parent directory. None if the event was raised
 
295
              on the watched item itself. This field is always provided
 
296
              even if the string is ''.
 
297
      - pathname (str): Concatenation of 'path' and 'name'.
 
298
      - src_pathname (str): Only present for IN_MOVED_TO events and only in
 
299
              the case where IN_MOVED_FROM events are watched too. Holds the
 
300
              source pathname from where pathname was moved from.
 
301
      - cookie (int): Cookie.
 
302
      - dir (bool): True if the event was raised against a directory.
 
303
 
 
304
    """
 
305
    def __init__(self, raw):
 
306
        """
 
307
        Concretely, this is the raw event plus inferred infos.
 
308
        """
 
309
        _Event.__init__(self, raw)
 
310
        self.maskname = EventsCodes.maskname(self.mask)
 
311
        if COMPATIBILITY_MODE:
 
312
            self.event_name = self.maskname
 
313
        try:
 
314
            if self.name:
 
315
                self.pathname = os.path.abspath(os.path.join(self.path,
 
316
                                                             self.name))
 
317
            else:
 
318
                self.pathname = os.path.abspath(self.path)
 
319
        except AttributeError, err:
 
320
            # Usually it is not an error some events are perfectly valids
 
321
            # despite the lack of these attributes.
 
322
            logging.debug(err)
 
323
 
 
324
 
 
325
class ProcessEventError(PyinotifyError):
 
326
    """
 
327
    ProcessEventError Exception. Raised on ProcessEvent error.
 
328
    """
 
329
    def __init__(self, err):
 
330
        """
 
331
        @param err: Exception error description.
 
332
        @type err: string
 
333
        """
 
334
        PyinotifyError.__init__(self, err)
 
335
 
 
336
 
 
337
class _ProcessEvent:
 
338
    """
 
339
    Abstract processing event class.
 
340
    """
 
341
    def __call__(self, event):
 
342
        """
 
343
        To behave like a functor the object must be callable.
 
344
        This method is a dispatch method. Its lookup order is:
 
345
          1. process_MASKNAME method
 
346
          2. process_FAMILY_NAME method
 
347
          3. otherwise calls process_default
 
348
 
 
349
        @param event: Event to be processed.
 
350
        @type event: Event object
 
351
        @return: By convention when used from the ProcessEvent class:
 
352
                 - Returning False or None (default value) means keep on
 
353
                 executing next chained functors (see chain.py example).
 
354
                 - Returning True instead means do not execute next
 
355
                   processing functions.
 
356
        @rtype: bool
 
357
        @raise ProcessEventError: Event object undispatchable,
 
358
                                  unknown event.
 
359
        """
 
360
        stripped_mask = event.mask - (event.mask & IN_ISDIR)
 
361
        maskname = EventsCodes.ALL_VALUES.get(stripped_mask)
 
362
        if maskname is None:
 
363
            raise ProcessEventError("Unknown mask 0x%08x" % stripped_mask)
 
364
 
 
365
        # 1- look for process_MASKNAME
 
366
        meth = getattr(self, 'process_' + maskname, None)
 
367
        if meth is not None:
 
368
            return meth(event)
 
369
        # 2- look for process_FAMILY_NAME
 
370
        meth = getattr(self, 'process_IN_' + maskname.split('_')[1], None)
 
371
        if meth is not None:
 
372
            return meth(event)
 
373
        # 3- default call method process_default
 
374
        return self.process_default(event)
 
375
 
 
376
    def __repr__(self):
 
377
        return '<%s>' % self.__class__.__name__
 
378
 
 
379
 
 
380
class ProcessEvent(_ProcessEvent):
 
381
    """
 
382
    Process events objects, can be specialized via subclassing, thus its
 
383
    behavior can be overriden:
 
384
 
 
385
    Note: you should not override __init__ in your subclass instead define
 
386
    a my_init() method, this method will be called automatically from the
 
387
    constructor of this class with its optionals parameters.
 
388
 
 
389
      1. Provide specialized individual methods, e.g. process_IN_DELETE for
 
390
         processing a precise type of event (e.g. IN_DELETE in this case).
 
391
      2. Or/and provide methods for processing events by 'family', e.g.
 
392
         process_IN_CLOSE method will process both IN_CLOSE_WRITE and
 
393
         IN_CLOSE_NOWRITE events (if process_IN_CLOSE_WRITE and
 
394
         process_IN_CLOSE_NOWRITE aren't defined though).
 
395
      3. Or/and override process_default for catching and processing all
 
396
         the remaining types of events.
 
397
    """
 
398
    pevent = None
 
399
 
 
400
    def __init__(self, pevent=None, **kargs):
 
401
        """
 
402
        Enable chaining of ProcessEvent instances.
 
403
 
 
404
        @param pevent: Optional callable object, will be called on event
 
405
                       processing (before self).
 
406
        @type pevent: callable
 
407
        @param kargs: This constructor is implemented as a template method
 
408
                      delegating its optionals keyworded arguments to the
 
409
                      method my_init().
 
410
        @type kargs: dict
 
411
        """
 
412
        self.pevent = pevent
 
413
        self.my_init(**kargs)
 
414
 
 
415
    def my_init(self, **kargs):
 
416
        """
 
417
        This method is called from ProcessEvent.__init__(). This method is
 
418
        empty here and must be redefined to be useful. In effect, if you
 
419
        need to specifically initialize your subclass' instance then you
 
420
        just have to override this method in your subclass. Then all the
 
421
        keyworded arguments passed to ProcessEvent.__init__() will be
 
422
        transmitted as parameters to this method. Beware you MUST pass
 
423
        keyword arguments though.
 
424
 
 
425
        @param kargs: optional delegated arguments from __init__().
 
426
        @type kargs: dict
 
427
        """
 
428
        pass
 
429
 
 
430
    def __call__(self, event):
 
431
        stop_chaining = False
 
432
        if self.pevent is not None:
 
433
            # By default methods return None so we set as guideline
 
434
            # that methods asking for stop chaining must explicitely
 
435
            # return non None or non False values, otherwise the default
 
436
            # behavior will be to accept chain call to the corresponding
 
437
            # local method.
 
438
            stop_chaining = self.pevent(event)
 
439
        if not stop_chaining:
 
440
            return _ProcessEvent.__call__(self, event)
 
441
 
 
442
    def nested_pevent(self):
 
443
        return self.pevent
 
444
 
 
445
    def process_IN_Q_OVERFLOW(self, event):
 
446
        """
 
447
        By default this method only reports warning messages, you can
 
448
        overredide it by subclassing ProcessEvent and implement your own
 
449
        process_IN_Q_OVERFLOW method. The actions you can take on receiving
 
450
        this event is either to update the variable max_queued_events in order
 
451
        to handle more simultaneous events or to modify your code in order to
 
452
        accomplish a better filtering diminishing the number of raised events.
 
453
        Because this method is defined, IN_Q_OVERFLOW will never get
 
454
        transmitted as arguments to process_default calls.
 
455
 
 
456
        @param event: IN_Q_OVERFLOW event.
 
457
        @type event: dict
 
458
        """
 
459
        logging.warning('Event queue overflowed.')
 
460
 
 
461
    def process_default(self, event):
 
462
        """
 
463
        Default processing event method. By default does nothing. Subclass
 
464
        ProcessEvent and redefine this method in order to modify its behavior.
 
465
 
 
466
        @param event: Event to be processed. Can be of any type of events but
 
467
                      IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW).
 
468
        @type event: Event instance
 
469
        """
 
470
        pass
 
471
 
 
472
 
 
473
class PrintAllEvents(ProcessEvent):
 
474
    """
 
475
    Dummy class used to print events strings representations. For instance this
 
476
    class is used from command line to print all received events to stdout.
 
477
    """
 
478
    def my_init(self, out=None):
 
479
        """
 
480
        @param out: Where events will be written.
 
481
        @type out: Object providing a valid file object interface.
 
482
        """
 
483
        if out is None:
 
484
            out = sys.stdout
 
485
        self._out = out
 
486
 
 
487
    def process_default(self, event):
 
488
        """
 
489
        Writes event string representation to file object provided to
 
490
        my_init().
 
491
 
 
492
        @param event: Event to be processed. Can be of any type of events but
 
493
                      IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW).
 
494
        @type event: Event instance
 
495
        """
 
496
        self._out.write(str(event))
 
497
        self._out.write('\n')
 
498
        self._out.flush()
 
499
 
 
500
 
 
501
class WatchManagerError(Exception):
 
502
    """
 
503
    WatchManager Exception. Raised on error encountered on watches
 
504
    operations.
 
505
 
 
506
    """
 
507
    def __init__(self, msg, wmd):
 
508
        """
 
509
        @param msg: Exception string's description.
 
510
        @type msg: string
 
511
        @param wmd: This dictionary contains the wd assigned to paths of the
 
512
                    same call for which watches were successfully added.
 
513
        @type wmd: dict
 
514
        """
 
515
        self.wmd = wmd
 
516
        Exception.__init__(self, msg)