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: 6125 $" 
 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 - def start(self):
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 # no change 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 # deletion 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
152 - def stop(self):
153 """Stop checking for file changes. 154 """ 155 self._delayedCall.cancel() 156 self._reset()
157
158 - def getFileData(self):
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
174 - def isNewFileStable(self, fName, fData):
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
192 - def getFilesToStat(self):
193 """ 194 @returns: sequence of filename 195 """ 196 raise NotImplementedError
197
198 -class DirectoryWatcher(BaseWatcher):
199 """ 200 Directory Watcher 201 Watches a directory for new files. 202 """ 203
204 - def __init__(self, path, ignorefiles=(), timeout=30):
205 BaseWatcher.__init__(self, timeout) 206 self.path = path 207 self._ignorefiles = ignorefiles
208
209 - def getFilesToStat(self):
210 return [os.path.join(self.path, f) 211 for f in os.listdir(self.path) 212 if f not in self._ignorefiles]
213
214 -class FilesWatcher(BaseWatcher):
215 """ 216 Watches a collection of files for modifications. 217 """ 218
219 - def __init__(self, files, timeout=30):
220 BaseWatcher.__init__(self, timeout) 221 self._files = files
222
223 - def getFilesToStat(self):
224 return self._files
225