1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 """
16 Provides path and process monitoring classes.
17 """
18
19 import os
20 import errno
21 from hashlib import sha256
22 from time import sleep
23 from threading import Thread, RLock
24 from logging import getLogger
25
26 log = getLogger(__name__)
27
28
30 """
31 Path monitor.
32 @ivar __paths: A list of paths to monitor.
33 @type __paths: dict path:(mtime, digest, cb)
34 @ivar __mutex: The mutex.
35 @type __mutex: RLock
36 @ivar __thread: The optional thread. see: start().
37 @type __thread: Thread
38 """
39
41 self.__paths = {}
42 self.__mutex = RLock()
43 self.__thread = None
44
45 - def add(self, path, cb):
46 """
47 Add a path to be monitored.
48 @param path: An absolute path to monitor.
49 @type path: str
50 @param cb: A listener.
51 @type cb: callable
52 """
53 self.__lock()
54 try:
55 self.__paths[path] = [0, None, cb]
56 finally:
57 self.__unlock()
58
60 """
61 Delete a path to be monitored.
62 @param path: An absolute path to monitor.
63 @type path: str
64 """
65 self.__lock()
66 try:
67 try:
68 del self.__paths[path]
69 except KeyError:
70 pass
71 finally:
72 self.__unlock()
73
74 - def start(self, precision=1):
75 """
76 Start the monitor thread.
77 @param precision: The precision (how often to check).
78 @type precision: float
79 @return: self
80 @rtype: L{PathMonitor}
81 """
82 self.__lock()
83 try:
84 if self.__thread:
85 raise Exception, 'already started'
86 thread = MonitorThread(self, precision)
87 thread.start()
88 self.__thread = thread
89 return self
90 finally:
91 self.__unlock()
92
94 """
95 Join the monitoring thread.
96 """
97 if not self.__thread:
98 raise Exception, 'not started'
99 self.__thread.join()
100
102 """
103 Check paths and notify.
104 """
105 self.__lock()
106 try:
107 paths = self.__paths.items()
108 finally:
109 self.__unlock()
110 for k,v in paths:
111 self.__sniff(k, v)
112
114 """
115 Sniff and compare the stats of the file at the
116 specified I{path}.
117 First, check the modification time, if different, then
118 check the I{hash} of the file content to see if it really
119 changed. If changed, notify the registered listener.
120 @param path: The path of the file to sniff.
121 @type path: str
122 @param stat: The cached stat (mtime, digest, cb)
123 @type stat: tuple
124 """
125 try:
126 f = File(path)
127 mtime = f.mtime()
128 if mtime == stat[0]:
129 return
130 digest = f.digest()
131 if (digest and stat[1]) and (digest == stat[1]):
132 return
133 self.__notify(path, stat[2])
134 stat[0] = mtime
135 stat[1] = digest
136 except:
137 log.exception(path)
138
140 """
141 Safely invoke registered callback.
142 @param path: The path of the changed file.
143 @type path: str
144 @param cb: A registered callback.
145 @type cb: callable
146 """
147 try:
148 cb(path)
149 except:
150 log.exception(path)
151
154
157
158
160 """
161 Safe file operations.
162 @ivar path: The path.
163 @type path: str
164 @ivar fp: The python file object.
165 @type fp: fileobj
166 """
167
169 """
170 @param path: The file path.
171 @type path: str
172 """
173 self.path = path
174 self.fp = None
175
177 """
178 Open (if not already open)
179 """
180 if not self.fp:
181 self.fp = open(self.path)
182
184 """
185 Close (if not already closed)
186 """
187 if self.fp:
188 self.fp.close()
189 self.fp = None
190
192 """
193 Read (n) bytes.
194 @param n: The bytes to read.
195 @type n: int
196 @return: the bytes read.
197 @rtype: buffer
198 """
199 return self.fp.read(n)
200
202 """
203 Get modification time.
204 @return: mtime
205 @rtype: int
206 """
207 try:
208 return os.path.getmtime(self.path)
209 except OSError:
210 pass
211 except:
212 log.exception(self.path)
213 return 0
214
216 """
217 Get the SHA256 hex digest for content.
218 @return: the hexdigest.
219 @rtype: str
220 """
221 try:
222 self.open()
223 h = sha256()
224 while True:
225 s = self.read(10240)
226 if s:
227 h.update(s)
228 else:
229 break
230 self.close()
231 return h.hexdigest()
232 except IOError:
233 pass
234 except:
235 log.exception(self.path)
236 return None
237
240
241
243 """
244 Monitor thread.
245 @ivar monitor: A monitor object.
246 @type monitor: Monitor
247 @ivar precision: The level of percision (seconds).
248 @type precision: float
249 """
250
251 - def __init__(self, monitor, precision):
252 """
253 @param monitor: A monitor object.
254 @type monitor: Monitor
255 @param precision: The level of percision (seconds).
256 @type precision: float
257 """
258 Thread.__init__(self, name='PathMonitor%s' % precision)
259 self.monitor = monitor
260 self.precision = precision
261 self.setDaemon(True)
262
264 """
265 Thread main run().
266 """
267 monitor = self.monitor
268 while True:
269 monitor.check()
270 sleep(self.precision)
271
272
273
274
275 if __name__ == '__main__':
277 print 'changed: %s' % path
278 from logging import basicConfig
279 basicConfig()
280 pmon = PathMonitor()
281 pmon.add('/tmp/jeff.repo', changed)
282 pmon.start()
283 pmon.join()
284