Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/tags/cherrypy-3.0.0/cherrypy/lib/sessions.py

Revision 1472 (checked in by fumanchu, 2 years ago)

Docstring for lib.sessions.init.

  • Property svn:eol-style set to native
Line 
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 class Session(object):
37     """A CherryPy dict-like Session object (one per request).
38     
39     id: current session ID.
40     expiration_time (datetime): when the current session will expire.
41     timeout (minutes): used to calculate expiration_time from now.
42     clean_freq (minutes): the poll rate for expired session cleanup.
43     locked: If True, this session instance has exclusive read/write access
44         to session data.
45     loaded: If True, data has been retrieved from storage. This should
46         happen automatically on the first attempt to access session data.
47     """
48    
49     clean_thread = None
50    
51     def __init__(self, id=None, **kwargs):
52         self.locked = False
53         self.loaded = False
54         self._data = {}
55        
56         for k, v in kwargs.iteritems():
57             setattr(self, k, v)
58        
59         self.id = id
60         while self.id is None:
61             self.id = self.generate_id()
62             # Assert that the generated id is not already stored.
63             if self._load() is not None:
64                 self.id = None
65    
66     def clean_interrupt(cls):
67         """Stop the expired-session cleaning timer."""
68         if cls.clean_thread:
69             cls.clean_thread.cancel()
70             cls.clean_thread.join()
71             cls.clean_thread = None
72     clean_interrupt = classmethod(clean_interrupt)
73    
74     def clean_up(self):
75         """Clean up expired sessions."""
76         pass
77    
78     try:
79         os.urandom(20)
80     except (AttributeError, NotImplementedError):
81         # os.urandom not available until Python 2.4. Fall back to random.random.
82         def generate_id(self):
83             """Return a new session id."""
84             return sha.new('%s' % random.random()).hexdigest()
85     else:
86         def generate_id(self):
87             """Return a new session id."""
88             return os.urandom(20).encode('hex')
89    
90     def save(self):
91         """Save session data."""
92         try:
93             # If session data has never been loaded then it's never been
94             #   accessed: no need to delete it
95             if self.loaded:
96                 t = datetime.timedelta(seconds = self.timeout * 60)
97                 expiration_time = datetime.datetime.now() + t
98                 self._save(expiration_time)
99            
100         finally:
101             if self.locked:
102                 # Always release the lock if the user didn't release it
103                 self.release_lock()
104    
105     def load(self):
106         """Copy stored session data into this session instance."""
107         data = self._load()
108         # data is either None or a tuple (session_data, expiration_time)
109         if data is None or data[1] < datetime.datetime.now():
110             # Expired session: flush session data (but keep the same id)
111             self._data = {}
112         else:
113             self._data = data[0]
114         self.loaded = True
115        
116         # Stick the clean_thread in the class, not the instance.
117         # The instances are created and destroyed per-request.
118         cls = self.__class__
119         if not cls.clean_thread:
120             cherrypy.engine.on_stop_engine_list.append(cls.clean_interrupt)
121             # clean_up is in instancemethod and not a classmethod,
122             # so tool config can be accessed inside the method.
123             t = PerpetualTimer(self.clean_freq, self.clean_up)
124             t.setName("CP Session Cleanup")
125             cls.clean_thread = t
126             t.start()
127    
128     def delete(self):
129         """Delete stored session data."""
130         self._delete()
131    
132     def __getitem__(self, key):
133         if not self.loaded: self.load()
134         return self._data[key]
135    
136     def __setitem__(self, key, value):
137         if not self.loaded: self.load()
138         self._data[key] = value
139    
140     def __delitem__(self, key):
141         if not self.loaded: self.load()
142         del self._data[key]
143    
144     def __contains__(self, key):
145         if not self.loaded: self.load()
146         return key in self._data
147    
148     def has_key(self, key):
149         if not self.loaded: self.load()
150         return self._data.has_key(key)
151    
152     def get(self, key, default=None):
153         if not self.loaded: self.load()
154         return self._data.get(key, default)
155    
156     def update(self, d):
157         if not self.loaded: self.load()
158         self._data.update(d)
159    
160     def setdefault(self, key, default=None):
161         if not self.loaded: self.load()
162         return self._data.setdefault(key, default)
163    
164     def clear(self):
165         if not self.loaded: self.load()
166         self._data.clear()
167    
168     def keys(self):
169         if not self.loaded: self.load()
170         return self._data.keys()
171    
172     def items(self):
173         if not self.loaded: self.load()
174         return self._data.items()
175    
176     def values(self):
177         if not self.loaded: self.load()
178         return self._data.values()
179
180
181 class RamSession(Session):
182    
183     # Class-level objects. Don't rebind these!
184     cache = {}
185     locks = {}
186    
187     def clean_up(self):
188         """Clean up expired sessions."""
189         now = datetime.datetime.now()
190         for id, (data, expiration_time) in self.cache.items():
191             if expiration_time < now:
192                 try:
193                     del self.cache[id]
194                 except KeyError:
195                     pass
196                 try:
197                     del self.locks[id]
198                 except KeyError:
199                     pass
200    
201     def _load(self):
202         return self.cache.get(self.id)
203    
204     def _save(self, expiration_time):
205         self.cache[self.id] = (self._data, expiration_time)
206    
207     def _delete(self):
208         del self.cache[self.id]
209    
210     def acquire_lock(self):
211         self.locked = True
212         self.locks.setdefault(self.id, threading.RLock()).acquire()
213    
214     def release_lock(self):
215         self.locks[self.id].release()
216         self.locked = False
217
218
219 class FileSession(Session):
220     """ Implementation of the File backend for sessions
221     
222     storage_path: the folder where session data will be saved. Each session
223         will be saved as pickle.dump(data, expiration_time) in its own file;
224         the filename will be self.SESSION_PREFIX + self.id.
225     """
226    
227     SESSION_PREFIX = 'session-'
228     LOCK_SUFFIX = '.lock'
229    
230     def setup(self):
231         # Warn if any lock files exist at startup.
232         lockfiles = [fname for fname in os.listdir(self.storage_path)
233                      if (fname.startswith(self.SESSION_PREFIX)
234                          and fname.endswith(self.LOCK_SUFFIX))]
235         if lockfiles:
236             plural = ('', 's')[len(lockfiles) > 1]
237             warn("%s session lockfile%s found at startup. If you are "
238                  "only running one process, then you may need to "
239                  "manually delete the lockfiles found at %r."
240                  % (len(lockfiles), plural,
241                     os.path.abspath(self.storage_path)))
242    
243     def _get_file_path(self):
244         return os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
245    
246     def _load(self, path=None):
247         if path is None:
248             path = self._get_file_path()
249         try:
250             f = open(path, "rb")
251             try:
252                 return pickle.load(f)
253             finally:
254                 f.close()
255         except (IOError, EOFError):
256             return None
257    
258     def _save(self, expiration_time):
259         f = open(self._get_file_path(), "wb")
260         try:
261             pickle.dump((self._data, expiration_time), f)
262         finally:
263             f.close()
264    
265     def _delete(self):
266         try:
267             os.unlink(self._get_file_path())
268         except OSError:
269             pass
270    
271     def acquire_lock(self, path=None):
272         if path is None:
273             path = self._get_file_path()
274         path += self.LOCK_SUFFIX
275         while True:
276             try:
277                 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
278             except OSError:
279                 time.sleep(0.1)
280             else:
281                 os.close(lockfd)
282                 break
283         self.locked = True
284    
285     def release_lock(self, path=None):
286         if path is None:
287             path = self._get_file_path()
288         os.unlink(path + self.LOCK_SUFFIX)
289         self.locked = False
290    
291     def clean_up(self):
292         """Clean up expired sessions."""
293         now = datetime.datetime.now()
294         # Iterate over all session files in self.storage_path
295         for fname in os.listdir(self.storage_path):
296             if (fname.startswith(self.SESSION_PREFIX)
297                 and not fname.endswith(self.LOCK_SUFFIX)):
298                 # We have a session file: lock and load it and check
299                 #   if it's expired. If it fails, nevermind.
300                 path = os.path.join(self.storage_path, fname)
301                 self.acquire_lock(path)
302                 try:
303                     contents = self._load(path)
304                     # _load returns None on IOError
305                     if contents is not None:
306                         data, expiration_time = contents
307                         if expiration_time < now:
308                             # Session expired: deleting it
309                             os.unlink(path)
310                 finally:
311                     self.release_lock(path)
312
313
314 class PostgresqlSession(Session):
315     """ Implementation of the PostgreSQL backend for sessions. It assumes
316         a table like this:
317
318             create table session (
319                 id varchar(40),
320                 data text,
321                 expiration_time timestamp
322             )
323     
324     You must provide your own get_db function.
325     """
326    
327     def __init__(self):
328         self.db = self.get_db()
329         self.cursor = self.db.cursor()
330    
331     def __del__(self):
332         if self.cursor:
333             self.cursor.close()
334         self.db.commit()
335    
336     def _load(self):
337         # Select session data from table
338         self.cursor.execute('select data, expiration_time from session '
339                             'where id=%s', (self.id,))
340         rows = self.cursor.fetchall()
341         if not rows:
342             return None
343        
344         pickled_data, expiration_time = rows[0]
345         data = pickle.loads(pickled_data)
346         return data, expiration_time
347    
348     def _save(self, expiration_time):
349         pickled_data = pickle.dumps(self._data)
350         self.cursor.execute('update session set data = %s, '
351                             'expiration_time = %s where id = %s',
352                             (pickled_data, expiration_time, self.id))
353    
354     def _delete(self):
355         self.cursor.execute('delete from session where id=%s', (self.id,))
356    
357     def acquire_lock(self):
358         # We use the "for update" clause to lock the row
359         self.locked = True
360         self.cursor.execute('select id from session where id=%s for update',
361                             (self.id,))
362    
363     def release_lock(self):
364         # We just close the cursor and that will remove the lock
365         #   introduced by the "for update" clause
366         self.cursor.close()
367         self.locked = False
368    
369     def clean_up(self):
370         """Clean up expired sessions."""
371         self.cursor.execute('delete from session where expiration_time < %s',
372                             (datetime.datetime.now(),))
373
374
375 # Hook functions (for CherryPy tools)
376
377 def save():
378     """Save any changed session data."""
379     # Guard against running twice
380     if hasattr(cherrypy.request, "_sessionsaved"):
381         return
382     cherrypy.request._sessionsaved = True
383    
384     if cherrypy.response.stream:
385         # If the body is being streamed, we have to save the data
386         #   *after* the response has been written out
387         cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save)
388     else:
389         # If the body is not being streamed, we save the data now
390         # (so we can release the lock).
391         if isinstance(cherrypy.response.body, types.GeneratorType):
392             cherrypy.response.collapse_body()
393         cherrypy.session.save()
394 save.failsafe = True
395
396 def close():
397     """Close the session object for this request."""
398     sess = cherrypy.session
399     if sess.locked:
400         # If the session is still locked we release the lock
401         sess.release_lock()
402 close.failsafe = True
403 close.priority = 90
404
405
406 _def_session = RamSession()
407
408 def init(storage_type='ram', path=None, path_header=None, name='session_id',
409          timeout=60, domain=None, secure=False, locking='implicit',
410          clean_freq=5, **kwargs):
411     """Initialize session object (using cookies).
412     
413     storage_type: one of 'ram', 'file', 'postgresql'. This will be used
414         to look up the corresponding class in cherrypy.lib.sessions
415         globals. For example, 'file' will use the FileSession class.
416     path: the 'path' value to stick in the response cookie metadata.
417     path_header: if 'path' is None (the default), then the response
418         cookie 'path' will be pulled from request.headers[path_header].
419     name: the name of the cookie.
420     timeout: the expiration timeout for the cookie.
421     domain: the cookie domain.
422     secure: if False (the default) the cookie 'secure' value will not
423         be set. If True, the cookie 'secure' value will be set (to 1).
424     locking: If 'implicit' (the default), this function will lock the
425         session for you. If 'explicit' (or any other value), you need
426         to call cherrypy.session.acquire_lock() yourself before
427         using session data.
428     clean_freq (minutes): the poll rate for expired session cleanup.
429     
430     Any additional kwargs will be bound to the new Session instance,
431     and may be specific to the storage type. See the subclass of Session
432     you're using for more information.
433     """
434    
435     request = cherrypy.request
436    
437     # Guard against running twice
438     if hasattr(cherrypy._serving, "session"):
439         return
440    
441     # Check if request came with a session ID
442     id = None
443     if name in request.cookie:
444         id = request.cookie[name].value
445    
446     # Create and attach a new Session instance to cherrypy._serving.
447     # It will possess a reference to (and lock, and lazily load)
448     # the requested session data.
449     storage_class = storage_type.title() + 'Session'
450     kwargs['timeout'] = timeout
451     kwargs['clean_freq'] = clean_freq
452     cherrypy._serving.session = sess = globals()[storage_class](id, **kwargs)
453    
454     if not hasattr(cherrypy, "session"):
455         cherrypy.session = cherrypy._ThreadLocalProxy('session', _def_session)
456         if hasattr(sess, "setup"):
457             sess.setup()
458    
459     if locking == 'implicit':
460         sess.acquire_lock()
461    
462     # Set response cookie
463     cookie = cherrypy.response.cookie
464     cookie[name] = sess.id
465     cookie[name]['path'] = path or request.headers.get(path_header) or '/'
466    
467     # We'd like to use the "max-age" param as indicated in
468     # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
469     # save it to disk and the session is lost if people close
470     # the browser. So we have to use the old "expires" ... sigh ...
471 ##    cookie[name]['max-age'] = timeout * 60
472     if timeout:
473         cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
474     if domain is not None:
475         cookie[name]['domain'] = domain
476     if secure:
477         cookie[name]['secure'] = 1
478
479 def expire():
480     """Expire the current session cookie."""
481     name = cherrypy.request.config.get('tools.sessions.name', 'session_id')
482     one_year = 60 * 60 * 24 * 365
483     exp = time.gmtime(time.time() - one_year)
484     t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp)
485     cherrypy.response.cookie[name]['expires'] = t
486
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets