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