1 # watcher.py - high-level interfaces to the Linux inotify subsystem |
|
2 |
|
3 # Copyright 2006 Bryan O'Sullivan <bos@serpentine.com> |
|
4 |
|
5 # This library is free software; you can redistribute it and/or modify |
|
6 # it under the terms of version 2.1 of the GNU Lesser General Public |
|
7 # License, or any later version. |
|
8 |
|
9 '''High-level interfaces to the Linux inotify subsystem. |
|
10 |
|
11 The inotify subsystem provides an efficient mechanism for file status |
|
12 monitoring and change notification. |
|
13 |
|
14 The watcher class hides the low-level details of the inotify |
|
15 interface, and provides a Pythonic wrapper around it. It generates |
|
16 events that provide somewhat more information than raw inotify makes |
|
17 available. |
|
18 |
|
19 The autowatcher class is more useful, as it automatically watches |
|
20 newly-created directories on your behalf.''' |
|
21 |
|
22 __author__ = "Bryan O'Sullivan <bos@serpentine.com>" |
|
23 |
|
24 import _inotify as inotify |
|
25 import array |
|
26 import errno |
|
27 import fcntl |
|
28 import os |
|
29 import termios |
|
30 |
|
31 |
|
32 class event(object): |
|
33 '''Derived inotify event class. |
|
34 |
|
35 The following fields are available: |
|
36 |
|
37 mask: event mask, indicating what kind of event this is |
|
38 |
|
39 cookie: rename cookie, if a rename-related event |
|
40 |
|
41 path: path of the directory in which the event occurred |
|
42 |
|
43 name: name of the directory entry to which the event occurred |
|
44 (may be None if the event happened to a watched directory) |
|
45 |
|
46 fullpath: complete path at which the event occurred |
|
47 |
|
48 wd: watch descriptor that triggered this event''' |
|
49 |
|
50 __slots__ = ( |
|
51 'cookie', |
|
52 'fullpath', |
|
53 'mask', |
|
54 'name', |
|
55 'path', |
|
56 'raw', |
|
57 'wd', |
|
58 ) |
|
59 |
|
60 def __init__(self, raw, path): |
|
61 self.path = path |
|
62 self.raw = raw |
|
63 if raw.name: |
|
64 self.fullpath = path + '/' + raw.name |
|
65 else: |
|
66 self.fullpath = path |
|
67 |
|
68 self.wd = raw.wd |
|
69 self.mask = raw.mask |
|
70 self.cookie = raw.cookie |
|
71 self.name = raw.name |
|
72 |
|
73 def __repr__(self): |
|
74 r = repr(self.raw) |
|
75 return 'event(path=' + repr(self.path) + ', ' + r[r.find('(') + 1:] |
|
76 |
|
77 |
|
78 _event_props = { |
|
79 'access': 'File was accessed', |
|
80 'modify': 'File was modified', |
|
81 'attrib': 'Attribute of a directory entry was changed', |
|
82 'close_write': 'File was closed after being written to', |
|
83 'close_nowrite': 'File was closed without being written to', |
|
84 'open': 'File was opened', |
|
85 'moved_from': 'Directory entry was renamed from this name', |
|
86 'moved_to': 'Directory entry was renamed to this name', |
|
87 'create': 'Directory entry was created', |
|
88 'delete': 'Directory entry was deleted', |
|
89 'delete_self': 'The watched directory entry was deleted', |
|
90 'move_self': 'The watched directory entry was renamed', |
|
91 'unmount': 'Directory was unmounted, and can no longer be watched', |
|
92 'q_overflow': 'Kernel dropped events due to queue overflow', |
|
93 'ignored': 'Directory entry is no longer being watched', |
|
94 'isdir': 'Event occurred on a directory', |
|
95 } |
|
96 |
|
97 for k, v in _event_props.iteritems(): |
|
98 mask = getattr(inotify, 'IN_' + k.upper()) |
|
99 def getter(self): |
|
100 return self.mask & mask |
|
101 getter.__name__ = k |
|
102 getter.__doc__ = v |
|
103 setattr(event, k, property(getter, doc=v)) |
|
104 |
|
105 del _event_props |
|
106 |
|
107 |
|
108 class watcher(object): |
|
109 '''Provide a Pythonic interface to the low-level inotify API. |
|
110 |
|
111 Also adds derived information to each event that is not available |
|
112 through the normal inotify API, such as directory name.''' |
|
113 |
|
114 __slots__ = ( |
|
115 'fd', |
|
116 '_paths', |
|
117 '_wds', |
|
118 ) |
|
119 |
|
120 def __init__(self): |
|
121 '''Create a new inotify instance.''' |
|
122 |
|
123 self.fd = inotify.init() |
|
124 self._paths = {} |
|
125 self._wds = {} |
|
126 |
|
127 def fileno(self): |
|
128 '''Return the file descriptor this watcher uses. |
|
129 |
|
130 Useful for passing to select and poll.''' |
|
131 |
|
132 return self.fd |
|
133 |
|
134 def add(self, path, mask): |
|
135 '''Add or modify a watch. |
|
136 |
|
137 Return the watch descriptor added or modified.''' |
|
138 |
|
139 path = os.path.normpath(path) |
|
140 wd = inotify.add_watch(self.fd, path, mask) |
|
141 self._paths[path] = wd, mask |
|
142 self._wds[wd] = path, mask |
|
143 return wd |
|
144 |
|
145 def remove(self, wd): |
|
146 '''Remove the given watch.''' |
|
147 |
|
148 inotify.remove_watch(self.fd, wd) |
|
149 self._remove(wd) |
|
150 |
|
151 def _remove(self, wd): |
|
152 path_mask = self._wds.pop(wd, None) |
|
153 if path_mask is not None: |
|
154 self._paths.pop(path_mask[0]) |
|
155 |
|
156 def path(self, path): |
|
157 '''Return a (watch descriptor, event mask) pair for the given path. |
|
158 |
|
159 If the path is not being watched, return None.''' |
|
160 |
|
161 return self._paths.get(path) |
|
162 |
|
163 def wd(self, wd): |
|
164 '''Return a (path, event mask) pair for the given watch descriptor. |
|
165 |
|
166 If the watch descriptor is not valid or not associated with |
|
167 this watcher, return None.''' |
|
168 |
|
169 return self._wds.get(wd) |
|
170 |
|
171 def read(self, bufsize=None): |
|
172 '''Read a list of queued inotify events. |
|
173 |
|
174 If bufsize is zero, only return those events that can be read |
|
175 immediately without blocking. Otherwise, block until events are |
|
176 available.''' |
|
177 |
|
178 events = [] |
|
179 for evt in inotify.read(self.fd, bufsize): |
|
180 events.append(event(evt, self._wds[evt.wd][0])) |
|
181 if evt.mask & inotify.IN_IGNORED: |
|
182 self._remove(evt.wd) |
|
183 elif evt.mask & inotify.IN_UNMOUNT: |
|
184 self.close() |
|
185 return events |
|
186 |
|
187 def close(self): |
|
188 '''Shut down this watcher. |
|
189 |
|
190 All subsequent method calls are likely to raise exceptions.''' |
|
191 |
|
192 os.close(self.fd) |
|
193 self.fd = None |
|
194 self._paths = None |
|
195 self._wds = None |
|
196 |
|
197 def __len__(self): |
|
198 '''Return the number of active watches.''' |
|
199 |
|
200 return len(self._paths) |
|
201 |
|
202 def __iter__(self): |
|
203 '''Yield a (path, watch descriptor, event mask) tuple for each |
|
204 entry being watched.''' |
|
205 |
|
206 for path, (wd, mask) in self._paths.iteritems(): |
|
207 yield path, wd, mask |
|
208 |
|
209 def __del__(self): |
|
210 if self.fd is not None: |
|
211 os.close(self.fd) |
|
212 |
|
213 ignored_errors = [errno.ENOENT, errno.EPERM, errno.ENOTDIR] |
|
214 |
|
215 def add_iter(self, path, mask, onerror=None): |
|
216 '''Add or modify watches over path and its subdirectories. |
|
217 |
|
218 Yield each added or modified watch descriptor. |
|
219 |
|
220 To ensure that this method runs to completion, you must |
|
221 iterate over all of its results, even if you do not care what |
|
222 they are. For example: |
|
223 |
|
224 for wd in w.add_iter(path, mask): |
|
225 pass |
|
226 |
|
227 By default, errors are ignored. If optional arg "onerror" is |
|
228 specified, it should be a function; it will be called with one |
|
229 argument, an OSError instance. It can report the error to |
|
230 continue with the walk, or raise the exception to abort the |
|
231 walk.''' |
|
232 |
|
233 # Add the IN_ONLYDIR flag to the event mask, to avoid a possible |
|
234 # race when adding a subdirectory. In the time between the |
|
235 # event being queued by the kernel and us processing it, the |
|
236 # directory may have been deleted, or replaced with a different |
|
237 # kind of entry with the same name. |
|
238 |
|
239 submask = mask | inotify.IN_ONLYDIR |
|
240 |
|
241 try: |
|
242 yield self.add(path, mask) |
|
243 except OSError, err: |
|
244 if onerror and err.errno not in self.ignored_errors: |
|
245 onerror(err) |
|
246 for root, dirs, names in os.walk(path, topdown=False, onerror=onerror): |
|
247 for d in dirs: |
|
248 try: |
|
249 yield self.add(root + '/' + d, submask) |
|
250 except OSError, err: |
|
251 if onerror and err.errno not in self.ignored_errors: |
|
252 onerror(err) |
|
253 |
|
254 def add_all(self, path, mask, onerror=None): |
|
255 '''Add or modify watches over path and its subdirectories. |
|
256 |
|
257 Return a list of added or modified watch descriptors. |
|
258 |
|
259 By default, errors are ignored. If optional arg "onerror" is |
|
260 specified, it should be a function; it will be called with one |
|
261 argument, an OSError instance. It can report the error to |
|
262 continue with the walk, or raise the exception to abort the |
|
263 walk.''' |
|
264 |
|
265 return [w for w in self.add_iter(path, mask, onerror)] |
|
266 |
|
267 |
|
268 class autowatcher(watcher): |
|
269 '''watcher class that automatically watches newly created directories.''' |
|
270 |
|
271 __slots__ = ( |
|
272 'addfilter', |
|
273 ) |
|
274 |
|
275 def __init__(self, addfilter=None): |
|
276 '''Create a new inotify instance. |
|
277 |
|
278 This instance will automatically watch newly created |
|
279 directories. |
|
280 |
|
281 If the optional addfilter parameter is not None, it must be a |
|
282 callable that takes one parameter. It will be called each time |
|
283 a directory is about to be automatically watched. If it returns |
|
284 True, the directory will be watched if it still exists, |
|
285 otherwise, it will be skipped.''' |
|
286 |
|
287 super(autowatcher, self).__init__() |
|
288 self.addfilter = addfilter |
|
289 |
|
290 _dir_create_mask = inotify.IN_ISDIR | inotify.IN_CREATE |
|
291 |
|
292 def read(self, bufsize=None): |
|
293 events = super(autowatcher, self).read(bufsize) |
|
294 for evt in events: |
|
295 if evt.mask & self._dir_create_mask == self._dir_create_mask: |
|
296 if self.addfilter is None or self.addfilter(evt): |
|
297 parentmask = self._wds[evt.wd][1] |
|
298 # See note about race avoidance via IN_ONLYDIR above. |
|
299 mask = parentmask | inotify.IN_ONLYDIR |
|
300 try: |
|
301 self.add_all(evt.fullpath, mask) |
|
302 except OSError, err: |
|
303 if err.errno not in self.ignored_errors: |
|
304 raise |
|
305 return events |
|
306 |
|
307 |
|
308 class threshold(object): |
|
309 '''Class that indicates whether a file descriptor has reached a |
|
310 threshold of readable bytes available. |
|
311 |
|
312 This class is not thread-safe.''' |
|
313 |
|
314 __slots__ = ( |
|
315 'fd', |
|
316 'threshold', |
|
317 '_iocbuf', |
|
318 ) |
|
319 |
|
320 def __init__(self, fd, threshold=1024): |
|
321 self.fd = fd |
|
322 self.threshold = threshold |
|
323 self._iocbuf = array.array('i', [0]) |
|
324 |
|
325 def readable(self): |
|
326 '''Return the number of bytes readable on this file descriptor.''' |
|
327 |
|
328 fcntl.ioctl(self.fd, termios.FIONREAD, self._iocbuf, True) |
|
329 return self._iocbuf[0] |
|
330 |
|
331 def __call__(self): |
|
332 '''Indicate whether the number of readable bytes has met or |
|
333 exceeded the threshold.''' |
|
334 |
|
335 return self.readable() >= self.threshold |
|