Inserted __future__ import of with_statement to be Jython (that is, 2.5) compatible.
[wrw.git] / wrw / session.py
CommitLineData
1f9d8560 1from __future__ import with_statement
b409a338 2import threading, time, pickle, random, os
1f61bf31 3import cookie, env
b409a338
FT
4
5__all__ = ["db", "get"]
6
7def hexencode(str):
8 ret = ""
9 for byte in str:
10 ret += "%02X" % (ord(byte),)
11 return ret
12
13def gennonce(length):
14 nonce = ""
15 for i in xrange(length):
16 nonce += chr(random.randint(0, 255))
17 return nonce
18
19class session(object):
b65f311b 20 def __init__(self, lock, expire = 86400 * 7):
b409a338
FT
21 self.id = hexencode(gennonce(16))
22 self.dict = {}
b65f311b 23 self.lock = lock
b409a338
FT
24 self.ctime = self.atime = self.mtime = int(time.time())
25 self.expire = expire
26 self.dctl = set()
27 self.dirtyp = False
28
29 def dirty(self):
30 for d in self.dctl:
31 if d.sessdirty():
32 return True
33 return self.dirtyp
34
35 def frozen(self):
36 for d in self.dctl:
37 d.sessfrozen()
38 self.dirtyp = False
39
40 def __getitem__(self, key):
41 return self.dict[key]
42
43 def get(self, key, default = None):
44 return self.dict.get(key, default)
45
46 def __setitem__(self, key, value):
47 self.dict[key] = value
48 if hasattr(value, "sessdirty"):
49 self.dctl.add(value)
50 else:
51 self.dirtyp = True
52
53 def __delitem__(self, key):
54 old = self.dict.pop(key)
55 if old in self.dctl:
56 self.dctl.remove(old)
57 self.dirtyp = True
58
59 def __contains__(self, key):
60 return key in self.dict
61
62 def __getstate__(self):
63 ret = []
64 for k, v in self.__dict__.items():
65 if k == "lock": continue
66 ret.append((k, v))
67 return ret
68
69 def __setstate__(self, st):
70 for k, v in st:
71 self.__dict__[k] = v
b65f311b 72 # The proper lock is set by the thawer
b409a338 73
b9e22c33
FT
74 def __repr__(self):
75 return "<session %s>" % self.id
76
b409a338 77class db(object):
f84a3f10 78 def __init__(self, backdb = None, cookiename = "wrwsess", path = "/"):
b409a338
FT
79 self.live = {}
80 self.cookiename = cookiename
81 self.path = path
82 self.lock = threading.Lock()
b409a338
FT
83 self.cthread = None
84 self.freezetime = 3600
f84a3f10 85 self.backdb = backdb
b409a338
FT
86
87 def clean(self):
88 now = int(time.time())
89 with self.lock:
b65f311b
FT
90 clist = self.live.keys()
91 for sessid in clist:
92 with self.lock:
93 try:
94 entry = self.live[sessid]
95 except KeyError:
96 continue
97 with entry[0]:
98 rm = False
99 if entry[1] == "retired":
100 pass
101 elif entry[1] is None:
102 pass
103 else:
104 sess = entry[1]
105 if sess.atime + self.freezetime < now:
106 try:
107 if sess.dirty():
108 self.freeze(sess)
109 except:
110 if sess.atime + sess.expire < now:
111 rm = True
112 else:
113 rm = True
114 if rm:
115 entry[1] = "retired"
116 with self.lock:
117 del self.live[sessid]
b409a338
FT
118
119 def cleanloop(self):
120 try:
188da534 121 while True:
b409a338
FT
122 time.sleep(300)
123 self.clean()
188da534
FT
124 if len(self.live) == 0:
125 break
b409a338
FT
126 finally:
127 with self.lock:
128 self.cthread = None
129
b65f311b
FT
130 def _fetch(self, sessid):
131 while True:
132 now = int(time.time())
133 with self.lock:
134 if sessid in self.live:
135 entry = self.live[sessid]
136 else:
137 entry = self.live[sessid] = [threading.RLock(), None]
138 with entry[0]:
139 if isinstance(entry[1], session):
140 entry[1].atime = now
141 return entry[1]
142 elif entry[1] == "retired":
143 continue
144 elif entry[1] is None:
145 try:
146 thawed = self.thaw(sessid)
147 if thawed.atime + thawed.expire < now:
148 raise KeyError()
149 thawed.lock = entry[0]
150 thawed.atime = now
151 entry[1] = thawed
152 return thawed
153 finally:
154 if entry[1] is None:
155 entry[1] = "retired"
156 with self.lock:
157 del self.live[sessid]
158 else:
159 raise Exception("Illegal session entry: " + repr(entry[1]))
160
dc7155d6 161 def checkclean(self):
b409a338
FT
162 with self.lock:
163 if self.cthread is None:
164 self.cthread = threading.Thread(target = self.cleanloop)
165 self.cthread.setDaemon(True)
166 self.cthread.start()
dc7155d6 167
afd93253
FT
168 def mksession(self, req):
169 return session(threading.RLock())
170
171 def mkcookie(self, req, sess):
c6e56d74
FT
172 cookie.add(req, self.cookiename, sess.id,
173 path=self.path,
174 expires=cookie.cdate(time.time() + sess.expire))
afd93253 175
dc7155d6
FT
176 def fetch(self, req):
177 now = int(time.time())
178 sessid = cookie.get(req, self.cookiename)
179 new = False
b65f311b
FT
180 try:
181 if sessid is None:
182 raise KeyError()
183 sess = self._fetch(sessid)
184 except KeyError:
afd93253 185 sess = self.mksession(req)
b65f311b 186 new = True
e70341b2
FT
187
188 def ckfreeze(req):
189 if sess.dirty():
bce33109 190 if new:
afd93253 191 self.mkcookie(req, sess)
bce33109 192 with self.lock:
b65f311b 193 self.live[sess.id] = [sess.lock, sess]
e70341b2 194 try:
e70341b2
FT
195 self.freeze(sess)
196 except:
197 pass
dc7155d6 198 self.checkclean()
e70341b2 199 req.oncommit(ckfreeze)
b409a338
FT
200 return sess
201
b409a338 202 def thaw(self, sessid):
f84a3f10
FT
203 if self.backdb is None:
204 raise KeyError()
b409a338
FT
205 data = self.backdb[sessid]
206 try:
207 return pickle.loads(data)
208 except Exception, e:
209 raise KeyError()
210
211 def freeze(self, sess):
f84a3f10
FT
212 if self.backdb is None:
213 raise TypeError()
b65f311b
FT
214 with sess.lock:
215 data = pickle.dumps(sess, -1)
216 self.backdb[sess.id] = data
b409a338
FT
217 sess.frozen()
218
f84a3f10
FT
219 def get(self, req):
220 return req.item(self.fetch)
221
b409a338
FT
222class dirback(object):
223 def __init__(self, path):
224 self.path = path
225
226 def __getitem__(self, key):
227 try:
228 with open(os.path.join(self.path, key)) as inf:
229 return inf.read()
230 except IOError:
231 raise KeyError(key)
232
233 def __setitem__(self, key, value):
234 if not os.path.exists(self.path):
235 os.makedirs(self.path)
236 with open(os.path.join(self.path, key), "w") as out:
237 out.write(value)
238
1f61bf31 239default = env.var(db(backdb = dirback(os.path.join("/tmp", "wrwsess-" + str(os.getuid())))))
b409a338
FT
240
241def get(req):
1f61bf31 242 return default.val.get(req)