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, 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 return os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
264
265 - def _load(self, path=None):
266 if path is None: 267 path = self._get_file_path() 268 try: 269 f = open(path, "rb") 270 try: 271 return pickle.load(f) 272 finally: 273 f.close() 274 except (IOError, EOFError): 275 return None
276
277 - def _save(self, expiration_time):
278 f = open(self._get_file_path(), "wb") 279 try: 280 pickle.dump((self._data, expiration_time), f) 281 finally: 282 f.close()
283
284 - def _delete(self):
285 try: 286 os.unlink(self._get_file_path()) 287 except OSError: 288 pass
289
290 - def acquire_lock(self, path=None):
291 if path is None: 292 path = self._get_file_path() 293 path += self.LOCK_SUFFIX 294 while True: 295 try: 296 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 297 except OSError: 298 time.sleep(0.1) 299 else: 300 os.close(lockfd) 301 break 302 self.locked = True
303
304 - def release_lock(self, path=None):
305 if path is None: 306 path = self._get_file_path() 307 os.unlink(path + self.LOCK_SUFFIX) 308 self.locked = False
309
310 - def clean_up(self):
311 """Clean up expired sessions.""" 312 now = datetime.datetime.now() 313 # Iterate over all session files in self.storage_path 314 for fname in os.listdir(self.storage_path): 315 if (fname.startswith(self.SESSION_PREFIX) 316 and not fname.endswith(self.LOCK_SUFFIX)): 317 # We have a session file: lock and load it and check 318 # if it's expired. If it fails, nevermind. 319 path = os.path.join(self.storage_path, fname) 320 self.acquire_lock(path) 321 try: 322 contents = self._load(path) 323 # _load returns None on IOError 324 if contents is not None: 325 data, expiration_time = contents 326 if expiration_time < now: 327 # Session expired: deleting it 328 os.unlink(path) 329 finally: