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