summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
blob: 32390c4eb75bb8f7829413e049bbb9b6cf1e3bad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
""" Inotify driver for file alteration events """

import logging
import operator
import os
import pyinotify
import sys
from Bcfg2.Compat import reduce
from Bcfg2.Server.FileMonitor import Event
from Bcfg2.Server.FileMonitor.Pseudo import Pseudo

logger = logging.getLogger(__name__)

class Inotify(Pseudo, pyinotify.ProcessEvent):
    __priority__ = 1
    action_map = {pyinotify.IN_CREATE: 'created',
                  pyinotify.IN_DELETE: 'deleted',
                  pyinotify.IN_MODIFY: 'changed',
                  pyinotify.IN_MOVED_FROM: 'deleted',
                  pyinotify.IN_MOVED_TO: 'created'}
    mask = reduce(lambda x, y: x | y, action_map.keys())

    def __init__(self, ignore=None, debug=False):
        Pseudo.__init__(self, ignore=ignore, debug=debug)
        self.wm = pyinotify.WatchManager()
        self.notifier = pyinotify.ThreadedNotifier(self.wm, self)
        self.notifier.start()
        self.event_filter = dict()
        self.watches_by_path = dict()

    def fileno(self):
        return self.wm.get_fd()

    def process_default(self, ievent):
        action = ievent.maskname
        for amask, aname in self.action_map.items():
            if ievent.mask & amask:
                action = aname
                break
        try:
            watch = self.wm.watches[ievent.wd]
        except KeyError:
            err = sys.exc_info()[1]
            logger.error("Error handling event for %s: Watch %s not found" %
                         (ievent.pathname, ievent.wd))
            return
        # FAM-style file monitors return the full path to the parent
        # directory that is being watched, relative paths to anything
        # contained within the directory. since we can't use inotify
        # to watch files directly, we have to sort of guess at whether
        # this watch was actually added on a file (and thus is in
        # self.event_filter because we're filtering out other events
        # on the directory) or was added directly on a directory.
        if (watch.path == ievent.pathname or ievent.wd in self.event_filter):
            path = ievent.pathname
        else:
            # relative path
            path = os.path.basename(ievent.pathname)
        # figure out the handleID.  start with the path of the event;
        # that should catch events on files that are watched directly.
        # (we have to watch the directory that a file is in, so this
        # lets us handle events on different files in the same
        # directory -- and thus under the same watch -- with different
        # objects.)  If the path to the event doesn't have a handler,
        # use the path of the watch itself.
        handleID = ievent.pathname
        if handleID not in self.handles:
            handleID = watch.path
        evt = Event(handleID, path, action)

        if (ievent.wd not in self.event_filter or
            ievent.pathname in self.event_filter[ievent.wd]):
            self.events.append(evt)

    def AddMonitor(self, path, obj):
        # strip trailing slashes
        path = path.rstrip("/")
        if not os.path.isdir(path):
            # inotify is a little wonky about watching files.  for
            # instance, if you watch /tmp/foo, and then do 'mv
            # /tmp/bar /tmp/foo', it processes that as a deletion of
            # /tmp/foo (which it technically _is_, but that's rather
            # useless -- we care that /tmp/foo changed, not that it
            # was first deleted and then created).  In order to
            # effectively watch a file, we have to watch the directory
            # it's in, and filter out events for other files in the
            # same directory that are not similarly watched.
            # watch_transient_file requires a Processor _class_, not
            # an object, so we can't have this object handle events,
            # which is Wrong, so we can't use that function.
            watch_path = os.path.dirname(path)
            is_dir = False
        else:
            watch_path = path
            is_dir = True

        # see if this path is already being watched
        try:
            wd = self.watches_by_path[watch_path]
        except KeyError:
            wd = self.wm.add_watch(watch_path, self.mask,
                                   quiet=False)[watch_path]
            self.watches_by_path[watch_path] = wd

        produce_exists = True
        if not is_dir:
            if wd not in self.event_filter:
                self.event_filter[wd] = [path]
            elif path not in self.event_filter[wd]:
                self.event_filter[wd].append(path)
            else:
                # we've been asked to watch a file that we're already
                # watching, so we don't need to produce 'exists'
                # events
                produce_exists = False

        # inotify doesn't produce initial 'exists' events, so we
        # inherit from Pseudo to produce those
        if produce_exists:
            return Pseudo.AddMonitor(self, path, obj, handleID=path)
        else:
            self.handles[path] = obj
            return path

    def shutdown(self):
        self.notifier.stop()