Package flumotion :: Package component :: Package base :: Module watcher
[hide private]

Source Code for Module flumotion.component.base.watcher

  1  # -*- Mode: Python -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 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: 7162 $" 
 30   
 31   
32 -class BaseWatcher(log.Loggable):
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
39 - def __init__(self, timeout):
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
50 - def _reset(self):
51 self._stableData = {} 52 self._changingData = {} 53 self._delayedCall = None
54
55 - def _subscribe(self, **events):
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
86 - def unsubscribe(self, id):
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 # FIXME: this API has tripped up two people thus far, including its 103 # author. make subscribe() call start() if necessary? 104
105 - def start(self):
106 """Start checking for file changes. 107 108 Subscribers will be notified asynchronously of changes to the 109 watched files. 110 """ 111 112 def checkFiles(): 113 self.log("checking for file changes") 114 new = self.getFileData() 115 changing = self._changingData 116 stable = self._stableData 117 for f in new: 118 if f not in changing: 119 if not f in stable and self.isNewFileStable(f, new[f]): 120 self.debug('file %s stable when noted', f) 121 stable[f] = new[f] 122 self.event('fileChanged', f) 123 elif f in stable and new[f] == stable[f]: 124 # no change 125 pass 126 else: 127 self.debug('change start noted for %s', f) 128 changing[f] = new[f] 129 else: 130 if new[f] == changing[f]: 131 self.debug('change finished for %s', f) 132 del changing[f] 133 stable[f] = new[f] 134 self.event('fileChanged', f) 135 else: 136 self.log('change continues for %s', f) 137 changing[f] = new[f] 138 for f in stable.keys(): 139 if f not in new: 140 # deletion 141 del stable[f] 142 self.debug('file %s has been deleted', f) 143 self.event('fileDeleted', f) 144 for f in changing.keys(): 145 if f not in new: 146 self.debug('file %s has been deleted', f) 147 del changing[f] 148 self._delayedCall = reactor.callLater(self.timeout, 149 checkFiles)
150 151 assert self._delayedCall is None 152 checkFiles()
153
154 - def stop(self):
155 """Stop checking for file changes. 156 """ 157 self._delayedCall.cancel() 158 self._reset()
159
160 - def getFileData(self):
161 """ 162 @returns: a dict, {filename => DATA} 163 DATA can be anything. In the default implementation it is a pair 164 of (mtime, size). 165 """ 166 ret = {} 167 for f in self.getFilesToStat(): 168 try: 169 stat = os.stat(f) 170 ret[f] = (stat.st_mtime, stat.st_size) 171 except OSError, e: 172 self.debug('could not read file %s: %s', f, 173 log.getExceptionMessage(e)) 174 return ret
175
176 - def isNewFileStable(self, fName, fData):
177 """ 178 Check if the file is already stable when being added to the 179 set of watched files. 180 181 @param fName: filename 182 @type fName: str 183 @param fData: DATA, as returned by L{getFileData} method. In 184 the default implementation it is a pair of 185 (mtime, size). 186 187 @rtype: bool 188 """ 189 __pychecker__ = 'unusednames=fName' 190 191 ret = fData[0] + self.timeout < time.time() 192 return ret
193
194 - def getFilesToStat(self):
195 """ 196 @returns: sequence of filename 197 """ 198 raise NotImplementedError
199 200
201 -class DirectoryWatcher(BaseWatcher):
202 """ 203 Directory Watcher 204 Watches a directory for new files. 205 """ 206
207 - def __init__(self, path, ignorefiles=(), timeout=30):
208 BaseWatcher.__init__(self, timeout) 209 self.path = path 210 self._ignorefiles = ignorefiles
211
212 - def getFilesToStat(self):
213 return [os.path.join(self.path, f) 214 for f in os.listdir(self.path) 215 if f not in self._ignorefiles]
216 217
218 -class FilesWatcher(BaseWatcher):
219 """ 220 Watches a collection of files for modifications. 221 """ 222
223 - def __init__(self, files, timeout=30):
224 BaseWatcher.__init__(self, timeout) 225 self._files = files
226
227 - def getFilesToStat(self):
228 return self._files
229