Package cherrypy :: Package lib :: Module sessions
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.sessions

  1  """Session implementation for CherryPy. 
  2   
  3  We use cherrypy.request to store some convenient variables as 
  4  well as data about the session for the current request. Instead of 
  5  polluting cherrypy.request we use a Session object bound to 
  6  cherrypy.session to store these variables. 
  7  """ 
  8   
  9  import datetime 
 10  import os 
 11  try: 
 12      import cPickle as pickle 
 13  except ImportError: 
 14      import pickle 
 15  import random 
 16  import sha 
 17  import time 
 18  import threading 
 19  import types 
 20  from warnings import warn 
 21   
 22  import cherrypy 
 23  from cherrypy.lib import http 
 24   
 25   
26 -class PerpetualTimer(threading._Timer):
27
28 - def run(self):
29 while True: 30 self.finished.wait(self.interval) 31 if self.finished.isSet(): 32 return 33 self.function(*self.args, **self.kwargs)
34 35 36 missing = object() 37
38 -class Session(object):
39 """A CherryPy dict-like Session object (one per request).""" 40 41 __metaclass__ = cherrypy._AttributeDocstrings 42 43 id = None 44 id__doc = "The current session ID." 45 46 timeout = 60 47 timeout__doc = "Number of minutes after which to delete session data." 48 49 locked = False 50 locked__doc = """ 51 If True, this session instance has exclusive read/write access 52 to session data.""" 53 54 loaded = False 55 loaded__doc = """ 56 If True, data has been retrieved from storage. This should happen 57 automatically on the first attempt to access session data.""" 58 59 clean_thread = None 60 clean_thread__doc = "Class-level PerpetualTimer which calls self.clean_up." 61 62 clean_freq = 5 63 clean_freq__doc = "The poll rate for expired session cleanup in minutes." 64
65 - def __init__(self, id=None, **kwargs):
66 self._data = {} 67 68 for k, v in kwargs.iteritems(): 69 setattr(self, k, v) 70 71 self.id = id 72 while self.id is None: 73 self.id = self.generate_id() 74 # Assert that the generated id is not already stored. 75 if self._load() is not None: 76 self.id = None
77
78 - def clean_interrupt(cls):
79 """Stop the expired-session cleaning timer.""" 80 if cls.clean_thread: 81 cls.clean_thread.cancel() 82 cls.clean_thread.join() 83 cls.clean_thread = None
84 clean_interrupt = classmethod(clean_interrupt) 85
86 - def clean_up(self):
87 """Clean up expired sessions.""" 88 pass
89 90 try: 91 os.urandom(20) 92 except (AttributeError, NotImplementedError): 93 # os.urandom not available until Python 2.4. Fall back to random.random.
94 - def generate_id(self):
95 """Return a new session id.""" 96 return sha.new('%s' % random.random()).hexdigest()
97 else:
98 - def generate_id(self):
99 """Return a new session id.""" 100 return os.urandom(20).encode('hex')
101
102 - def save(self):
103 """Save session data.""" 104 try: 105 # If session data has never been loaded then it's never been 106 # accessed: no need to delete it 107 if self.loaded: 108 t = datetime.timedelta(seconds = self.timeout * 60) 109 expiration_time = datetime.datetime.now() + t 110 self._save(expiration_time) 111 112 finally: 113 if self.locked: 114 # Always release the lock if the user didn't release it 115 self.release_lock()
116
117 - def load(self):
118 """Copy stored session data into this session instance.""" 119 data = self._load() 120 # data is either None or a tuple (session_data, expiration_time) 121 if data is None or data[1] < datetime.datetime.now(): 122 # Expired session: flush session data (but keep the same id) 123 self._data = {} 124 else: 125 self._data = data[0] 126 self.loaded = True 127 128 # Stick the clean_thread in the class, not the instance. 129 # The instances are created and destroyed per-request. 130 cls = self.__class__ 131 if not cls.clean_thread: 132 cherrypy.engine.on_stop_engine_list.append(cls.clean_interrupt) 133 # clean_up is in instancemethod and not a classmethod, 134 # so tool config can be accessed inside the method. 135 t = PerpetualTimer(self.clean_freq * 60, self.clean_up) 136 t.setName("CP Session Cleanup") 137 cls.clean_thread = t 138 t.start()
139
140 - def delete(self):
141 """Delete stored session data.""" 142 self._delete()
143
144 - def __getitem__(self, key):
145 if not self.loaded: self.load() 146 return self._data[key]
147
148 - def __setitem__(self, key, value):
149 if not self.loaded: self.load() 150 self._data[key] = value
151
152 - def __delitem__(self, key):
153 if not self.loaded: self.load() 154 del self._data[key]
155
156 - def pop(self, key, default=missing):
157 if not self.loaded: self.load() 158 if default is missing: 159 return self._data.pop(key) 160 else: 161 return self._data.pop(key, default)
162
163 - def __contains__(self, key):
164 if not self.loaded: self.load() 165 return key in self._data
166
167 - def has_key(self, key):
168 if not self.loaded: self.load() 169 return self._data.has_key(key)
170
171 - def get(self, key, default=None):
172 if not self.loaded: self.load() 173 return self._data.get(key, default)
174
175 - def update(self, d):
176 if not self.loaded: self.load() 177 self._data.update(d)
178
179 - def setdefault(self, key, default=None):
180 if not self.loaded: self.load() 181 return self._data.setdefault(key, default)
182
183 - def clear(self):
184 if not self.loaded: self.load() 185 self._data.clear()
186
187 - def keys(self):
188 if not self.loaded: self.load() 189 return self._data.keys()
190
191 - def items(self):
192 if not self.loaded: self.load() 193 return self._data.items()
194
195 - def values(self):
196 if not self.loaded: self.load() 197 return self._data.values()
198 199
200 -class RamSession(Session):
201 202 # Class-level objects. Don't rebind these! 203 cache = {} 204 locks = {} 205
206 - def clean_up(self):
207 """Clean up expired sessions.""" 208 now = datetime.datetime.now() 209 for id, (data, expiration_time) in self.cache.items(): 210 if expiration_time < now: 211 try: 212 del self.cache[id] 213 except KeyError: 214 pass 215 try: 216 del self.locks[id] 217 except KeyError: 218 pass
219
220 - def _load(self):
221 return self.cache.get(self.id)
222
223 - def _save(self, expiration_time):
224 self.cache[self.id] = (self._data, expiration_time)
225
226 - def _delete(self):
227 del self.cache[self.id]
228
229 - def acquire_lock(self):
230 self.locked = True 231 self.locks.setdefault(self.id, threading.RLock()).acquire()
232
233 - def release_lock(self):
234 self.locks[self.id].release() 235 self.locked = False
236 237
238 -class FileSession(Session):
239 """ Implementation of the File backend for sessions 240 241 storage_path: the folder where session data will be saved. Each session 242 will be saved as pickle.dump(data, expiration_time) in its own file; 243 the filename will be self.SESSION_PREFIX + self.id. 244 """ 245 246 SESSION_PREFIX = 'session-' 247 LOCK_SUFFIX = '.lock' 248
249 - def setup(self):
250 # Warn if any lock files exist at startup. 251 lockfiles = [fname for fname in os.listdir(self.storage_path) 252 if (fname.startswith(self.SESSION_PREFIX) 253 and fname.endswith(self.LOCK_SUFFIX))] 254 if lockfiles: 255 plural = ('', 's')[len(lockfiles) > 1] 256 warn("%s session lockfile%s found at startup. If you are " 257 "only running one process, then you may need to " 258 "manually delete the lockfiles found at %r." 259 % (len(lockfiles), plural, 260 os.path.abspath(self.storage_path)))
261
262 - def _get_file_path(self):
263 f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) 264 if not os.path.normpath(f).startswith(self.storage_path): 265 raise cherrypy.HTTPError(400, "Invalid session id in cookie.") 266 return f
267
268 - def _load(self, path=None):
269 if path is None: 270 path = self._get_file_path() 271 try: 272 f = open(path, "rb") 273 try: 274 return pickle.load(f) 275 finally: 276 f.close() 277 except (IOError, EOFError): 278 return None
279
280 - def _save(self, expiration_time):
281 f = open(self._get_file_path(), "wb") 282 try: 283 pickle.dump((self._data, expiration_time), f) 284 finally: 285 f.close()
286
287 - def _delete(self):
288 try: 289 os.unlink(self._get_file_path()) 290 except OSError: 291 pass
292
293 - def acquire_lock(self, path=None):
294 if path is None: 295 path = self._get_file_path() 296 path += self.LOCK_SUFFIX 297 while True: 298 try: 299 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 300 except OSError: 301 time.sleep(0.1) 302 else: 303 os.close(lockfd) 304 break 305 self.locked = True
306
307 - def release_lock(self, path=None):
308 if path is None: 309 path = self._get_file_path() 310 os.unlink(path + self.LOCK_SUFFIX) 311 self.locked = False
312
313 - def clean_up(self):
314 """Clean up expired sessions.""" 315 now = datetime.datetime.now() 316 # Iterate over all session files in self.storage_path 317 for fname in os.listdir(self.storage_path): 318 if (fname.startswith(self.SESSION_PREFIX) 319 and not fname.endswith(self.LOCK_SUFFIX)): 320 # We have a session file: lock and load it and check 321 # if it's expired. If it fails, nevermind. 322 path = os.path.join(self.storage_path, fname) 323 self.acquire_lock(path)