Changeset 597
- Timestamp:
- 09/03/05 05:43:19
- Files:
-
- trunk/cherrypy/__init__.py (modified) (2 diffs)
- trunk/cherrypy/lib/filter/sessionfilter.py (modified) (4 diffs)
- trunk/cherrypy/test/test_session_filter.py (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/cherrypy/__init__.py
r581 r597 33 33 __version__ = '2.1.0-beta' 34 34 35 import datetime 36 35 37 from _cperror import * 36 37 38 import config 38 39 import server … … 60 61 threadData = local() 61 62 63 # Create variables needed for session (see lib/sessionfilter.py for more info) 62 64 from lib.filter import sessionfilter 63 65 session = sessionfilter.SessionWrapper() 66 _sessionDataHolder = {} # Needed for RAM sessions only 67 _sessionLockDict = {} # Needed for RAM sessions only 68 _sessionLastCleanUpTime = datetime.datetime.now() 64 69 65 70 # decorator function for exposing methods trunk/cherrypy/lib/filter/sessionfilter.py
r582 r597 28 28 29 29 """ Session implementation for CherryPy. 30 We use cherrypy.threadData (td) to store some convenient variables as 31 well as data about the session for the current request. 30 We use cherrypy.threadData to store some convenient variables as 31 well as data about the session for the current request. Instead of 32 polluting cherrypy.threadData we use a dummy object called 33 cherrypy.threadData._session (sess) to store these variables. 32 34 33 35 Variables used to store config options: 34 - td._sessionTimeout: timeout delay for the session35 - td._sessionLocking: mechanism used to lock the session ('implicit' or 'explicit')36 - sess.sessionTimeout: timeout delay for the session 37 - sess.sessionLocking: mechanism used to lock the session ('implicit' or 'explicit') 36 38 37 39 Variables used to store temporary variables: 38 - td._sessionStorage (instance of the class implementing the backend)40 - sess.sessionStorage (instance of the class implementing the backend) 39 41 40 42 41 43 Variables used to store the session for the current request: 42 - td._sessionData: dictionnary containing the actual session data43 - td._sessionID: current session ID44 - td._expirationTime:time when the current session will expire44 - sess.sessionData: dictionary containing the actual session data 45 - sess.sessionID: current session ID 46 - sess.expirationTime: date/time when the current session will expire 45 47 46 48 Global variables (RAM backend only): 47 - cherrypy._sessionLockDict: diction nary containing the locks for all sessionIDs48 - cherrypy._sessionHolder: diction nary containing the data for all sessions49 - cherrypy._sessionLockDict: dictionary containing the locks for all sessionIDs 50 - cherrypy._sessionHolder: dictionary containing the data for all sessions 49 51 50 52 """ 51 53 54 import datetime 52 55 import sha 53 56 import os 54 57 import pickle 55 58 import random 59 import StringIO 60 import time 56 61 import threading 57 import t ime62 import types 58 63 59 64 import basefilter 60 65 61 # TODO: Clean up old sessions 62 # TODO: Release stale locks after a certain time 66 class EmptyClass: 67 """ An empty class """ 68 pass 69 70 class SessionDeadlockError(Exception): 71 """ Happens when a session can't acquire a lock after a 72 certain time 73 """ 74 pass 63 75 64 76 class SessionFilter(basefilter.BaseFilter): … … 66 78 # We have to dynamically import cherrypy because Python can't handle 67 79 # circular module imports :-( 68 global cherrypy , td80 global cherrypy 69 81 import cherrypy 70 td = cherrypy.threadData 71 if not cherrypy.config.get('sessionFilter.on', False): 72 td._sessionStorage = None 82 cherrypy.threadData._session = EmptyClass() 83 sess = cherrypy.threadData._session 84 now = datetime.datetime.now() 85 # Dont enable session if sessionFilter is off or if this is a 86 # request for static data 87 if (not cherrypy.config.get('sessionFilter.on', False)) or \ 88 cherrypy.config.get('staticFilter.on', False): 89 sess.sessionStorage = None 73 90 return 74 91 92 sess.locked = False # Not locked by default 93 75 94 # Read config options 76 td._sessionTimeout = \95 sess.sessionTimeout = \ 77 96 cherrypy.config.get('sessionFilter.timeout', 60) 78 97 79 td._sessionLocking = \98 sess.sessionLocking = \ 80 99 cherrypy.config.get('sessionFilter.locking', 'implicit') 100 101 sess.onCreateSession = \ 102 cherrypy.config.get('sessionFilter.onCreateSession', 103 lambda data: None) 104 105 sess.onDeleteSession = \ 106 cherrypy.config.get('sessionFilter.onDeleteSession', 107 lambda data: None) 108 109 cleanUpDelay = \ 110 cherrypy.config.get('sessionFilter.cleanUpDelay', 5) 81 111 82 112 cookieName = \ 83 113 cherrypy.config.get('sessionFilter.cookieName', 'sessionID') 84 114 115 sess.deadlockTimeout = \ 116 cherrypy.config.get('sessionFilter.deadlockTimeout', 30) 117 85 118 storage = cherrypy.config.get('sessionFilter.storageType', 'Ram') 86 storage = storage.capitalize() 87 # TODO: support custom storage types 88 td._sessionStorage = globals()[storage + 'Storage']() 119 storage = storage[0].upper() + storage[1:] 120 # TODO: support custom storage types (allow users to pass 121 # their own class through another config option) 122 sess.sessionStorage = globals()[storage + 'Storage']() 123 124 # Check if we need to clean up old sessions 125 if cherrypy._sessionLastCleanUpTime + \ 126 datetime.timedelta(seconds = cleanUpDelay * 60) < now: 127 sess.sessionStorage.cleanUp() 89 128 90 129 # Check if request came with a session ID 91 130 if cookieName in cherrypy.request.simpleCookie: 92 131 # It did: we try to load the session data 93 td._sessionID = cherrypy.request.simpleCookie[cookieName].value 94 data = td._sessionStorage.load(td._sessionID) 132 sess.sessionID = cherrypy.request.simpleCookie[cookieName].value 133 # If using implicit locking, acquire lock 134 if sess.sessionLocking == 'implicit': 135 sess.sessionData = {'_id': sess.sessionID} 136 sess.sessionStorage.acquireLock() 137 data = sess.sessionStorage.load(sess.sessionID) 95 138 # data is either None or a tuple (sessionData, expirationTime) 96 if data is None or data[1] < time.time():139 if data is None or data[1] < now: 97 140 # Expired session: flush session data (but keep the same 98 141 # sessionID) 99 td._sessionData = {}142 sess.sessionData = {'_id': sess.sessionID} 100 143 else: 101 td._sessionData = data[0]144 sess.sessionData = data[0] 102 145 else: 103 146 # No sessionID yet 104 td._sessionID = generateSessionID()105 td._sessionData = {}106 td._sessionData['_id'] = td._sessionID147 sess.sessionID = generateSessionID() 148 sess.sessionData = {'_id': sess.sessionID} 149 sess.onCreateSession(sess.sessionData) 107 150 # Set response cookie 108 cherrypy.response.simpleCookie[cookieName] = td._sessionID151 cherrypy.response.simpleCookie[cookieName] = sess.sessionID 109 152 cherrypy.response.simpleCookie[cookieName]['path'] = '/' 110 153 cherrypy.response.simpleCookie[cookieName]['max-age'] = \ 111 td._sessionTimeout * 60154 sess.sessionTimeout * 60 112 155 cherrypy.response.simpleCookie[cookieName]['version'] = 1 113 156 114 # If using implicit locking, acquire lock115 if td._sessionLocking == 'implicit':116 td._sessionStorage.acquireLock()117 118 157 def beforeFinalize(self): 119 if not td._sessionStorage: 158 def returnBodyAndSaveData(body, sess): 159 # If the body is a generator, we have to save the data 160 # *after* the generator has been consumed 161 if isinstance(body, types.GeneratorType): 162 for line in body: 163 yield line 164 165 # Save session data 166 expirationTime = datetime.datetime.now() + \ 167 datetime.timedelta(seconds = sess.sessionTimeout * 60) 168 sess.sessionStorage.save( 169 sess.sessionID, sess.sessionData, expirationTime) 170 if sess.locked: 171 # Always release the lock if the user didn't release it 172 sess.sessionStorage.releaseLock() 173 174 # If the body is not a generator, we save the data 175 # before the body is returned 176 if not isinstance(body, types.GeneratorType): 177 for line in body: 178 yield line 179 180 sess = cherrypy.threadData._session 181 if not sess.sessionStorage: 182 # Sessions are not enabled: do nothing 120 183 return 121 # Save session data 122 expirationTime = time.time() + td._sessionTimeout * 60 123 td._sessionStorage.save( 124 td._sessionID, (td._sessionData, expirationTime)) 125 try: 126 # Always try to release the lock at the end 127 td._sessionStorage.releaseLock() 128 except: 129 pass 184 185 # Make a wrapper around the body in order to save the session 186 # either before or after the body is returned 187 cherrypy.response.body = \ 188 returnBodyAndSaveData(cherrypy.response.body, sess) 189 130 190 131 191 def onEndResource(self): 132 try: 133 # Try to release the lock one more time at the very end (in 134 # case there was an error while processing the request 135 # or something) 136 td._sessionStorage.releaseLock() 137 except: 138 pass 192 sess = cherrypy.threadData._session 193 if getattr(sess, 'locked', None): 194 # If the session is still locked there probably was an 195 # error while processing the request. 196 # In that case we release the lock anyway. 197 sess.sessionStorage.releaseLock() 198 if getattr(sess, 'sessionStorage', None): 199 del sess.sessionStorage 139 200 140 201 class RamStorage: 141 202 """ Implementation of the RAM backend for sessions """ 142 def __init__(self):143 try:144 cherrypy._sessionDataHolder145 except:146 cherrypy._sessionDataHolder = {}147 try:148 cherrypy._sessionLockDict149 except:150 cherrypy._sessionLockDict = {}151 152 203 def load(self, id): 153 204 return cherrypy._sessionDataHolder.get(id) 154 def save(self, id, data ):155 cherrypy._sessionDataHolder[id] = data205 def save(self, id, data, expirationTime): 206 cherrypy._sessionDataHolder[id] = (data, expirationTime) 156 207 def acquireLock(self): 208 sess = cherrypy.threadData._session 157 209 id = cherrypy.session['_id'] 158 210 lock = cherrypy._sessionLockDict.get(id) … … 160 212 lock = threading.Lock() 161 213 cherrypy._sessionLockDict[id] = lock 162 lock.acquire() 214 startTime = time.time() 215 while True: 216 if lock.acquire(False): 217 break 218 if time.time() - startTime > sess.deadlockTimeout: 219 raise SessionDeadlockError() 220 sess.locked = True 163 221 def releaseLock(self): 222 sess = cherrypy.threadData._session 164 223 id = cherrypy.session['_id'] 165 224 cherrypy._sessionLockDict[id].release() 225 sess.locked = False 226 def cleanUp(self): 227 sess = cherrypy.threadData._session 228 toBeDeleted = [] 229 now = datetime.datetime.now() 230 for id, (data, expirationTime) in cherrypy._sessionDataHolder.iteritems(): 231 if expirationTime < now: 232 toBeDeleted.append(id) 233 for id in toBeDeleted: 234 sess.onDeleteSession(cherrypy._sessionDataHolder[id]) 235 del cherrypy._sessionDataHolder[id] 166 236 167 237 class FileStorage: 168 238 """ Implementation of the File backend for sessions """ 239 SESSION_PREFIX = 'session-' 240 LOCK_SUFFIX = '.lock' 169 241 def load(self, id): 170 242 filePath = self._getFilePath(id) … … 174 246 f.close() 175 247 return data 176 except :177 return (None, None)178 def save(self, id, data ):248 except IOError: 249 return None 250 def save(self, id, data, expirationTime): 179 251 filePath = self._getFilePath(id) 180 252 f = open(filePath, "wb") 181 pickle.dump( data, f)253 pickle.dump((data, expirationTime), f) 182 254 f.close() 183 255 def acquireLock(self): 184 # Use the OS to acquire a lock on the file. 185 # This means that if we have multiple CP processes it'll still 186 # work fine 256 sess = cherrypy.threadData._session 187 257 filePath = self._getFilePath(cherrypy.session['_id']) 188 lockFilePath = filePath + '.lock' 258 lockFilePath = filePath + self.LOCK_SUFFIX 259 self._lockFile(lockFilePath) 260 sess.locked = True 261 262 def releaseLock(self): 263 sess = cherrypy.threadData._session 264 filePath = self._getFilePath(cherrypy.session['_id']) 265 lockFilePath = filePath + self.LOCK_SUFFIX 266 self._unlockFile(lockFilePath) 267 sess.locked = False 268 269 def cleanUp(self): 270 sess = cherrypy.threadData._session 271 storagePath = cherrypy.config.get('sessionFilter.storagePath') 272 now = datetime.datetime.now() 273 # Iterate over all files in the dir/ and exclude non session files 274 # and lock files 275 for fname in os.listdir(storagePath): 276 if fname.startswith(self.SESSION_PREFIX) and \ 277 (not fname.endswith(self.LOCK_SUFFIX)): 278 # We have a session file: lock it, load it and check 279 # if it's expired 280 filePath = os.path.join(storagePath, fname) 281 lockFilePath = filePath + self.LOCK_SUFFIX 282 self._lockFile(lockFilePath) 283 try: 284 f = open(filePath, "rb") 285 (data, expirationTime) = pickle.load(f) 286 f.close() 287 if expirationTime < now: 288 # Session expired: deleting it 289 id = fname[len(self.SESSION_PREFIX):] 290 sess.onDeleteSession(data) 291 os.unlink(filePath) 292 except IOError: 293 # We can't access the file ... nevermind 294 pass 295 self._unlockFile(lockFilePath) 296 297 def _getFilePath(self, id): 298 storagePath = cherrypy.config.get('sessionFilter.storagePath') 299 fileName = self.SESSION_PREFIX + id 300 filePath = os.path.join(storagePath, fileName) 301 return filePath 302 303 def _lockFile(self, path): 304 sess = cherrypy.threadData._session 305 startTime = time.time() 189 306 while True: 190 307 try: 191 lockfd = os.open( lockFilePath, os.O_CREAT|os.O_WRONLY|os.O_EXCL)308 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 192 309 except OSError: 193 pass 310 if time.time() - startTime > sess.deadlockTimeout: 311 raise SessionDeadlockError() 194 312 else: 195 313 os.close(lockfd) 196 314 break 315 def _unlockFile(self, path): 316 os.unlink(path) 317 318 class PostgreSQLStorage: 319 """ Implementation of the PostgreSQL backend for sessions. It assumes 320 a table like this: 321 322 create table session ( 323 id varchar(40), 324 data text, 325 expiration_time timestamp 326 ) 327 """ 328 def __init__(self): 329 self.db = cherrypy.config.get('sessionFilter.getDB')() 330 self.cursor = self.db.cursor() 331 def __del__(self): 332 if self.cursor: 333 self.cursor.close() 334 self.db.commit() 335 def load(self, id): 336 # Select session data from table 337 self.cursor.execute( 338 'select data, expiration_time from session where id=%s', 339 (id,)) 340 rows = self.cursor.fetchall() 341 if not rows: 342 return None 343 data, expirationTime = rows[0] 344 # Unpickle data 345 f = StringIO.StringIO(data) 346 data = pickle.load(f) 347 return (data, expirationTime) 348 def save(self, id, data, expirationTime): 349 # Try to delete session if it was already there 350 self.cursor.execute( 351 'delete from session where id=%s', 352 (id,)) 353 # Pickle data 354 f = StringIO.StringIO() 355 pickle.dump(data, f) 356 # Insert new session data 357 self.cursor.execute( 358 'insert into session (id, data, expiration_time) values (%s, %s, %s)', 359 (id, f.getvalue(), expirationTime)) 360 361 def acquireLock(self): 362 # We use the "for update" clause to lock the row 363 self.cursor.execute( 364 'select id from session where id=%s for update', 365 (cherrypy.session['_id'],)) 197 366 198 367 def releaseLock(self): 199 filePath = self._getFilePath(cherrypy.session['_id']) 200 lockFilePath = filePath + '.lock' 201 os.unlink(lockFilePath) 202 203 def _getFilePath(self, id): 204 storagePath = cherrypy.config.get('sessionFilter.storagePath') 205 fileName = 'sessionFile-' + id 206 filePath = os.path.join(storagePath, fileName) 207 return filePath 208 368 # We just close the cursor and that will remove the lock 369 # introduced by the "for update" clause 370 self.cursor.close() 371 self.cursor = None 372 def cleanUp(self): 373 sess = cherrypy.threadData._session 374 now = datetime.datetime.now() 375 self.cursor.execute( 376 'select data from session where expiration_time < %s', 377 (now,)) 378 rows = self.cursor.fetchall() 379 for row in rows: 380 sess.onDeleteSession(row[0]) 381 self.cursor.execute( 382 'delete from session where expiration_time < %s', 383 (now,)) 209 384 210 385 def generateSessionID(): 211 """ Function to return a new sessionID """386 """ Return a new sessionID """ 212 387 return sha.new('%s' % random.random()).hexdigest() 213 388 214 389 # Users access sessions through cherrypy.session, but we want this 215 390 # to be thread-specific so we use a special wrapper that forwards 216 # calls to cherrypy.session to a thread-specific diction nary called217 # cherrypy.threadData._session Data391 # calls to cherrypy.session to a thread-specific dictionary called 392 # cherrypy.threadData._session.sessionData 218 393 class SessionWrapper(object): 219 394 def __getattribute__(self, name): 220 # Create thread-specific dictionnary if needed 221 try: 222 td._sessionData 223 except: 224 td._sessionData = {} 395 # Create thread-specific dictionary if needed 396 sess = cherrypy.threadData._session 397 sess.sessionData = getattr(sess, 'sessionData', {}) 225 398 if name == 'acquireLock': 226 return td._sessionStorage.acquireLock399 return sess.sessionStorage.acquireLock 227 400 elif name == 'releaseLock': 228 return td._sessionStorage.releaseLock229 return td._sessionData.__getattribute__(name)401 return sess.sessionStorage.releaseLock 402 return sess.sessionData.__getattribute__(name) 230 403 def __getitem__(self, *a, **b): 231 return td._sessionData.__getitem__(*a, **b) 404 sess = cherrypy.threadData._session 405 return sess.sessionData.__getitem__(*a, **b) 232 406 def __setitem__(self, *a, **b): 233 return td._sessionData.__setitem__(*a, **b) 407 sess = cherrypy.threadData._session 408 return sess.sessionData.__setitem__(*a, **b) 409 def __delitem__(self, *a, **b): 410 sess = cherrypy.threadData._session 411 return sess.sessionData.__delitem__(*a, **b) trunk/cherrypy/test/test_session_filter.py
r581 r597 27 27 """ 28 28 29 import cherrypy 29 import cherrypy, time, os 30 30 31 31 class Root: 32 def index(self):32 def testGen(self): 33 33 counter = cherrypy.session.get('counter', 0) + 1 34 34 cherrypy.session['counter'] = counter 35 35 yield str(counter) 36 index.exposed = True 36 testGen.exposed = True 37 def testStr(self): 38 counter = cherrypy.session.get('counter', 0) + 1 39 cherrypy.session['counter'] = counter 40 return str(counter) 41 testStr.exposed = True 37 42 38 43 cherrypy.root = Root() … … 41 46 'server.environment': 'production', 42 47 'sessionFilter.on': True, 48 'sessionFilter.storageType' : 'file', 49 'sessionFilter.storagePath' : '.', 43 50 }) 44 51 … … 48 55 49 56 def testSessionFilter(self): 50 self.getPage('/ ')57 self.getPage('/testStr') 51 58 self.assertBody('1') 52 self.getPage('/ ', self.cookies)59 self.getPage('/testGen', self.cookies) 53 60 self.assertBody('2') 54 self.getPage('/ ', self.cookies)61 self.getPage('/testStr', self.cookies) 55 62 self.assertBody('3') 63 cherrypy.config.update({ 64 'sessionFilter.storageType' : 'file', 65 }) 66 self.getPage('/testStr') 67 self.assertBody('1') 68 self.getPage('/testGen', self.cookies) 69 self.assertBody('2') 70 self.getPage('/testStr', self.cookies) 71 self.assertBody('3') 72 73 # Clean up session files 74 for fname in os.listdir('.'): 75 if fname.startswith('session-'): 76 os.unlink(fname) 77 56 78 57 79 if __name__ == "__main__":

