Mercurial > hg
annotate hgext/inotify/linux/watcher.py @ 9777:b2dfe76459dc
i18n-sv: add missing newlines to translations
author | Martin Geisler <mg@lazybytes.net> |
---|---|
date | Wed, 04 Nov 2009 22:14:26 +0100 |
parents | 1536501ade62 |
children | 25e572394f5c |
rev | line source |
---|---|
6239 | 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, incorporated herein by reference. | |
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 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
14 The watcher class hides the low-level details of the inotify |
6239 | 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 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
19 The autowatcher class is more useful, as it automatically watches |
6239 | 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 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
32 class event(object): |
6239 | 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 | |
6287
c86207d41512
Spacing cleanup
Thomas Arendsen Hein <thomas@intevation.de>
parents:
6239
diff
changeset
|
72 |
6239 | 73 def __repr__(self): |
74 r = repr(self.raw) | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
75 return 'event(path=' + repr(self.path) + ', ' + r[r.find('(')+1:] |
6239 | 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 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
103 setattr(event, k, property(getter, doc=v)) |
6239 | 104 |
105 del _event_props | |
106 | |
107 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
108 class watcher(object): |
6239 | 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. | |
6287
c86207d41512
Spacing cleanup
Thomas Arendsen Hein <thomas@intevation.de>
parents:
6239
diff
changeset
|
158 |
6239 | 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) | |
6287
c86207d41512
Spacing cleanup
Thomas Arendsen Hein <thomas@intevation.de>
parents:
6239
diff
changeset
|
170 |
6239 | 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): | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
180 events.append(event(evt, self._wds[evt.wd][0])) |
6239 | 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 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
268 class autowatcher(watcher): |
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
269 '''watcher class that automatically watches newly created directories.''' |
6239 | 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 beb skipped.''' | |
286 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
287 super(autowatcher, self).__init__() |
6239 | 288 self.addfilter = addfilter |
289 | |
290 _dir_create_mask = inotify.IN_ISDIR | inotify.IN_CREATE | |
291 | |
292 def read(self, bufsize=None): | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
293 events = super(autowatcher, self).read(bufsize) |
6239 | 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 | |
8385
1536501ade62
inotify: Coding Style: name classes in lowercase.
Nicolas Dumazet <nicdumz.commits@gmail.com>
parents:
6287
diff
changeset
|
308 class threshold(object): |
6239 | 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 |