Added more useful repr for sessions.
[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
b409a338
FT
160 def fetch(self, req):
161 now = int(time.time())
b409a338 162 sessid = cookie.get(req, self.cookiename)
e70341b2 163 new = False
b409a338
FT
164 with self.lock:
165 if self.cthread is None:
166 self.cthread = threading.Thread(target = self.cleanloop)
167 self.cthread.setDaemon(True)
168 self.cthread.start()
b65f311b
FT
169 try:
170 if sessid is None:
171 raise KeyError()
172 sess = self._fetch(sessid)
173 except KeyError:
174 sess = session(threading.RLock())
175 new = True
e70341b2
FT
176
177 def ckfreeze(req):
178 if sess.dirty():
bce33109
FT
179 if new:
180 cookie.add(req, self.cookiename, sess.id, self.path)
181 with self.lock:
b65f311b 182 self.live[sess.id] = [sess.lock, sess]
e70341b2 183 try:
e70341b2
FT
184 self.freeze(sess)
185 except:
186 pass
187 req.oncommit(ckfreeze)
b409a338
FT
188 return sess
189
b409a338 190 def thaw(self, sessid):
f84a3f10
FT
191 if self.backdb is None:
192 raise KeyError()
b409a338
FT
193 data = self.backdb[sessid]
194 try:
195 return pickle.loads(data)
196 except Exception, e:
197 raise KeyError()
198
199 def freeze(self, sess):
f84a3f10
FT
200 if self.backdb is None:
201 raise TypeError()
b65f311b
FT
202 with sess.lock:
203 data = pickle.dumps(sess, -1)
204 self.backdb[sess.id] = data
b409a338
FT
205 sess.frozen()
206
f84a3f10
FT
207 def get(self, req):
208 return req.item(self.fetch)
209
b409a338
FT
210class dirback(object):
211 def __init__(self, path):
212 self.path = path
213
214 def __getitem__(self, key):
215 try:
216 with open(os.path.join(self.path, key)) as inf:
217 return inf.read()
218 except IOError:
219 raise KeyError(key)
220
221 def __setitem__(self, key, value):
222 if not os.path.exists(self.path):
223 os.makedirs(self.path)
224 with open(os.path.join(self.path, key), "w") as out:
225 out.write(value)
226
1f61bf31 227default = env.var(db(backdb = dirback(os.path.join("/tmp", "wrwsess-" + str(os.getuid())))))
b409a338
FT
228
229def get(req):
1f61bf31 230 return default.val.get(req)