1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import os
23 import time
24
25 from twisted.internet import reactor
26
27 from flumotion.common import log
28
29 __version__ = "$Rev: 6125 $"
30
31
33 """I watch for file changes.
34
35 I am a base class for a file watcher. I can be specialized to watch
36 any set of files.
37 """
38
40 """Make a file watcher object.
41
42 @param timeout: timeout between checks, in seconds
43 @type timeout: int
44 """
45 self.timeout = timeout
46 self._reset()
47 self._subscribeId = 0
48 self.subscribers = {}
49
51 self._stableData = {}
52 self._changingData = {}
53 self._delayedCall = None
54
56 """Subscribe to events.
57
58 @param events: The events to subscribe to. Subclasses are
59 expected to formalize this dict, specifying which events they
60 support via declaring their kwargs explicitly.
61
62 @returns: A subscription ID that can later be passed to
63 unsubscribe().
64 """
65 sid = self._subscribeId
66 self._subscribeId += 1
67 self.subscribers[sid] = events
68 return sid
69
70 - def subscribe(self, fileChanged=None, fileDeleted=None):
71 """Subscribe to events.
72
73 @param fileChanged: A function to call when a file changes. This
74 function will only be called if the file's details (size, mtime)
75 do not change during the timeout period.
76 @type fileChanged: filename -> None
77 @param fileDeleted: A function to call when a file is deleted.
78 @type fileDeleted: filename -> None
79
80 @returns: A subscription ID that can later be passed to
81 unsubscribe().
82 """
83 return self._subscribe(fileChanged=fileChanged,
84 fileDeleted=fileDeleted)
85
87 """Unsubscribe from file change notifications.
88
89 @param id: Subscription ID received from subscribe()
90 """
91 del self.subscribers[id]
92
93 - def event(self, event, *args, **kwargs):
94 """Fire an event.
95
96 This method is intended for use by object implementations.
97 """
98 for s in self.subscribers.values():
99 if s[event]:
100 s[event](*args, **kwargs)
101
102
103
105 """Start checking for file changes.
106
107 Subscribers will be notified asynchronously of changes to the
108 watched files.
109 """
110 def checkFiles():
111 self.log("checking for file changes")
112 new = self.getFileData()
113 changing = self._changingData
114 stable = self._stableData
115 for f in new:
116 if f not in changing:
117 if not f in stable and self.isNewFileStable(f, new[f]):
118 self.debug('file %s stable when noted', f)
119 stable[f] = new[f]
120 self.event('fileChanged', f)
121 elif f in stable and new[f] == stable[f]:
122
123 pass
124 else:
125 self.debug('change start noted for %s', f)
126 changing[f] = new[f]
127 else:
128 if new[f] == changing[f]:
129 self.debug('change finished for %s', f)
130 del changing[f]
131 stable[f] = new[f]
132 self.event('fileChanged', f)
133 else:
134 self.log('change continues for %s', f)
135 changing[f] = new[f]
136 for f in stable.keys():
137 if f not in new:
138
139 del stable[f]
140 self.debug('file %s has been deleted', f)
141 self.event('fileDeleted', f)
142 for f in changing.keys():
143 if f not in new:
144 self.debug('file %s has been deleted', f)
145 del changing[f]
146 self._delayedCall = reactor.callLater(self.timeout,
147 checkFiles)
148
149 assert self._delayedCall is None
150 checkFiles()
151
153 """Stop checking for file changes.
154 """
155 self._delayedCall.cancel()
156 self._reset()
157
159 """
160 @returns: a dict, {filename => DATA}
161 DATA can be anything. In the default implementation it is a pair
162 of (mtime, size).
163 """
164 ret = {}
165 for f in self.getFilesToStat():
166 try:
167 stat = os.stat(f)
168 ret[f] = (stat.st_mtime, stat.st_size)
169 except OSError, e:
170 self.debug('could not read file %s: %s', f,
171 log.getExceptionMessage(e))
172 return ret
173
175 """
176 Check if the file is already stable when being added to the
177 set of watched files.
178
179 @param fName: filename
180 @type fName: str
181 @param fData: DATA, as returned by L{getFileData} method. In
182 the default implementation it is a pair of
183 (mtime, size).
184
185 @rtype: bool
186 """
187 __pychecker__ = 'unusednames=fName'
188
189 ret = fData[0] + self.timeout < time.time()
190 return ret
191
193 """
194 @returns: sequence of filename
195 """
196 raise NotImplementedError
197
199 """
200 Directory Watcher
201 Watches a directory for new files.
202 """
203
204 - def __init__(self, path, ignorefiles=(), timeout=30):
208
210 return [os.path.join(self.path, f)
211 for f in os.listdir(self.path)
212 if f not in self._ignorefiles]
213
215 """
216 Watches a collection of files for modifications.
217 """
218
222
225