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