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