##// END OF EJS Templates
hgweb: update _runwsgi try/except range to be valid...
marmoute -
r52189:3972d090 default
parent child Browse files
Show More
@@ -1,593 +1,593 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9
9
10 import gc
10 import gc
11 import os
11 import os
12 import time
12 import time
13
13
14 from ..i18n import _
14 from ..i18n import _
15
15
16 from .common import (
16 from .common import (
17 ErrorResponse,
17 ErrorResponse,
18 HTTP_SERVER_ERROR,
18 HTTP_SERVER_ERROR,
19 cspvalues,
19 cspvalues,
20 get_contact,
20 get_contact,
21 get_mtime,
21 get_mtime,
22 ismember,
22 ismember,
23 paritygen,
23 paritygen,
24 staticfile,
24 staticfile,
25 statusmessage,
25 statusmessage,
26 )
26 )
27
27
28 from .. import (
28 from .. import (
29 configitems,
29 configitems,
30 encoding,
30 encoding,
31 error,
31 error,
32 extensions,
32 extensions,
33 hg,
33 hg,
34 pathutil,
34 pathutil,
35 profiling,
35 profiling,
36 pycompat,
36 pycompat,
37 rcutil,
37 rcutil,
38 registrar,
38 registrar,
39 scmutil,
39 scmutil,
40 templater,
40 templater,
41 templateutil,
41 templateutil,
42 ui as uimod,
42 ui as uimod,
43 util,
43 util,
44 )
44 )
45
45
46 from . import (
46 from . import (
47 hgweb_mod,
47 hgweb_mod,
48 request as requestmod,
48 request as requestmod,
49 webutil,
49 webutil,
50 wsgicgi,
50 wsgicgi,
51 )
51 )
52 from ..utils import dateutil
52 from ..utils import dateutil
53
53
54
54
55 def cleannames(items):
55 def cleannames(items):
56 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
56 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
57
57
58
58
59 def findrepos(paths):
59 def findrepos(paths):
60 repos = []
60 repos = []
61 for prefix, root in cleannames(paths):
61 for prefix, root in cleannames(paths):
62 roothead, roottail = os.path.split(root)
62 roothead, roottail = os.path.split(root)
63 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
63 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
64 # /bar/ be served as as foo/N .
64 # /bar/ be served as as foo/N .
65 # '*' will not search inside dirs with .hg (except .hg/patches),
65 # '*' will not search inside dirs with .hg (except .hg/patches),
66 # '**' will search inside dirs with .hg (and thus also find subrepos).
66 # '**' will search inside dirs with .hg (and thus also find subrepos).
67 try:
67 try:
68 recurse = {b'*': False, b'**': True}[roottail]
68 recurse = {b'*': False, b'**': True}[roottail]
69 except KeyError:
69 except KeyError:
70 repos.append((prefix, root))
70 repos.append((prefix, root))
71 continue
71 continue
72 roothead = os.path.normpath(util.abspath(roothead))
72 roothead = os.path.normpath(util.abspath(roothead))
73 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
73 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
74 repos.extend(urlrepos(prefix, roothead, paths))
74 repos.extend(urlrepos(prefix, roothead, paths))
75 return repos
75 return repos
76
76
77
77
78 def urlrepos(prefix, roothead, paths):
78 def urlrepos(prefix, roothead, paths):
79 """yield url paths and filesystem paths from a list of repo paths
79 """yield url paths and filesystem paths from a list of repo paths
80
80
81 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
81 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
82 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
82 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
83 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
83 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
84 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
84 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
85 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
85 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
86 """
86 """
87 for path in paths:
87 for path in paths:
88 path = os.path.normpath(path)
88 path = os.path.normpath(path)
89 yield (
89 yield (
90 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
90 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
91 ).strip(b'/'), path
91 ).strip(b'/'), path
92
92
93
93
94 def readallowed(ui, req):
94 def readallowed(ui, req):
95 """Check allow_read and deny_read config options of a repo's ui object
95 """Check allow_read and deny_read config options of a repo's ui object
96 to determine user permissions. By default, with neither option set (or
96 to determine user permissions. By default, with neither option set (or
97 both empty), allow all users to read the repo. There are two ways a
97 both empty), allow all users to read the repo. There are two ways a
98 user can be denied read access: (1) deny_read is not empty, and the
98 user can be denied read access: (1) deny_read is not empty, and the
99 user is unauthenticated or deny_read contains user (or *), and (2)
99 user is unauthenticated or deny_read contains user (or *), and (2)
100 allow_read is not empty and the user is not in allow_read. Return True
100 allow_read is not empty and the user is not in allow_read. Return True
101 if user is allowed to read the repo, else return False."""
101 if user is allowed to read the repo, else return False."""
102
102
103 user = req.remoteuser
103 user = req.remoteuser
104
104
105 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
105 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
106 if deny_read and (not user or ismember(ui, user, deny_read)):
106 if deny_read and (not user or ismember(ui, user, deny_read)):
107 return False
107 return False
108
108
109 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
109 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
110 # by default, allow reading if no allow_read option has been set
110 # by default, allow reading if no allow_read option has been set
111 if not allow_read or ismember(ui, user, allow_read):
111 if not allow_read or ismember(ui, user, allow_read):
112 return True
112 return True
113
113
114 return False
114 return False
115
115
116
116
117 def rawindexentries(ui, repos, req, subdir=b''):
117 def rawindexentries(ui, repos, req, subdir=b''):
118 descend = ui.configbool(b'web', b'descend')
118 descend = ui.configbool(b'web', b'descend')
119 collapse = ui.configbool(b'web', b'collapse')
119 collapse = ui.configbool(b'web', b'collapse')
120 seenrepos = set()
120 seenrepos = set()
121 seendirs = set()
121 seendirs = set()
122 for name, path in repos:
122 for name, path in repos:
123
123
124 if not name.startswith(subdir):
124 if not name.startswith(subdir):
125 continue
125 continue
126 name = name[len(subdir) :]
126 name = name[len(subdir) :]
127 directory = False
127 directory = False
128
128
129 if b'/' in name:
129 if b'/' in name:
130 if not descend:
130 if not descend:
131 continue
131 continue
132
132
133 nameparts = name.split(b'/')
133 nameparts = name.split(b'/')
134 rootname = nameparts[0]
134 rootname = nameparts[0]
135
135
136 if not collapse:
136 if not collapse:
137 pass
137 pass
138 elif rootname in seendirs:
138 elif rootname in seendirs:
139 continue
139 continue
140 elif rootname in seenrepos:
140 elif rootname in seenrepos:
141 pass
141 pass
142 else:
142 else:
143 directory = True
143 directory = True
144 name = rootname
144 name = rootname
145
145
146 # redefine the path to refer to the directory
146 # redefine the path to refer to the directory
147 discarded = b'/'.join(nameparts[1:])
147 discarded = b'/'.join(nameparts[1:])
148
148
149 # remove name parts plus accompanying slash
149 # remove name parts plus accompanying slash
150 path = path[: -len(discarded) - 1]
150 path = path[: -len(discarded) - 1]
151
151
152 try:
152 try:
153 hg.repository(ui, path)
153 hg.repository(ui, path)
154 directory = False
154 directory = False
155 except (IOError, error.RepoError):
155 except (IOError, error.RepoError):
156 pass
156 pass
157
157
158 parts = [
158 parts = [
159 req.apppath.strip(b'/'),
159 req.apppath.strip(b'/'),
160 subdir.strip(b'/'),
160 subdir.strip(b'/'),
161 name.strip(b'/'),
161 name.strip(b'/'),
162 ]
162 ]
163 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
163 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
164
164
165 # show either a directory entry or a repository
165 # show either a directory entry or a repository
166 if directory:
166 if directory:
167 # get the directory's time information
167 # get the directory's time information
168 try:
168 try:
169 d = (get_mtime(path), dateutil.makedate()[1])
169 d = (get_mtime(path), dateutil.makedate()[1])
170 except OSError:
170 except OSError:
171 continue
171 continue
172
172
173 # add '/' to the name to make it obvious that
173 # add '/' to the name to make it obvious that
174 # the entry is a directory, not a regular repository
174 # the entry is a directory, not a regular repository
175 row = {
175 row = {
176 b'contact': b"",
176 b'contact': b"",
177 b'contact_sort': b"",
177 b'contact_sort': b"",
178 b'name': name + b'/',
178 b'name': name + b'/',
179 b'name_sort': name,
179 b'name_sort': name,
180 b'url': url,
180 b'url': url,
181 b'description': b"",
181 b'description': b"",
182 b'description_sort': b"",
182 b'description_sort': b"",
183 b'lastchange': d,
183 b'lastchange': d,
184 b'lastchange_sort': d[1] - d[0],
184 b'lastchange_sort': d[1] - d[0],
185 b'archives': templateutil.mappinglist([]),
185 b'archives': templateutil.mappinglist([]),
186 b'isdirectory': True,
186 b'isdirectory': True,
187 b'labels': templateutil.hybridlist([], name=b'label'),
187 b'labels': templateutil.hybridlist([], name=b'label'),
188 }
188 }
189
189
190 seendirs.add(name)
190 seendirs.add(name)
191 yield row
191 yield row
192 continue
192 continue
193
193
194 u = ui.copy()
194 u = ui.copy()
195 if rcutil.use_repo_hgrc():
195 if rcutil.use_repo_hgrc():
196 try:
196 try:
197 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
197 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
198 except Exception as e:
198 except Exception as e:
199 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
199 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
200 continue
200 continue
201
201
202 def get(section, name, default=uimod._unset):
202 def get(section, name, default=uimod._unset):
203 return u.config(section, name, default, untrusted=True)
203 return u.config(section, name, default, untrusted=True)
204
204
205 if u.configbool(b"web", b"hidden", untrusted=True):
205 if u.configbool(b"web", b"hidden", untrusted=True):
206 continue
206 continue
207
207
208 if not readallowed(u, req):
208 if not readallowed(u, req):
209 continue
209 continue
210
210
211 # update time with local timezone
211 # update time with local timezone
212 try:
212 try:
213 r = hg.repository(ui, path)
213 r = hg.repository(ui, path)
214 except IOError:
214 except IOError:
215 u.warn(_(b'error accessing repository at %s\n') % path)
215 u.warn(_(b'error accessing repository at %s\n') % path)
216 continue
216 continue
217 except error.RepoError:
217 except error.RepoError:
218 u.warn(_(b'error accessing repository at %s\n') % path)
218 u.warn(_(b'error accessing repository at %s\n') % path)
219 continue
219 continue
220 try:
220 try:
221 d = (get_mtime(r.spath), dateutil.makedate()[1])
221 d = (get_mtime(r.spath), dateutil.makedate()[1])
222 except OSError:
222 except OSError:
223 continue
223 continue
224
224
225 contact = get_contact(get)
225 contact = get_contact(get)
226 description = get(b"web", b"description")
226 description = get(b"web", b"description")
227 seenrepos.add(name)
227 seenrepos.add(name)
228 name = get(b"web", b"name", name)
228 name = get(b"web", b"name", name)
229 labels = u.configlist(b'web', b'labels', untrusted=True)
229 labels = u.configlist(b'web', b'labels', untrusted=True)
230 row = {
230 row = {
231 b'contact': contact or b"unknown",
231 b'contact': contact or b"unknown",
232 b'contact_sort': contact.upper() or b"unknown",
232 b'contact_sort': contact.upper() or b"unknown",
233 b'name': name,
233 b'name': name,
234 b'name_sort': name,
234 b'name_sort': name,
235 b'url': url,
235 b'url': url,
236 b'description': description or b"unknown",
236 b'description': description or b"unknown",
237 b'description_sort': description.upper() or b"unknown",
237 b'description_sort': description.upper() or b"unknown",
238 b'lastchange': d,
238 b'lastchange': d,
239 b'lastchange_sort': d[1] - d[0],
239 b'lastchange_sort': d[1] - d[0],
240 b'archives': webutil.archivelist(u, b"tip", url),
240 b'archives': webutil.archivelist(u, b"tip", url),
241 b'isdirectory': None,
241 b'isdirectory': None,
242 b'labels': templateutil.hybridlist(labels, name=b'label'),
242 b'labels': templateutil.hybridlist(labels, name=b'label'),
243 }
243 }
244
244
245 yield row
245 yield row
246
246
247
247
248 def _indexentriesgen(
248 def _indexentriesgen(
249 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
249 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
250 ):
250 ):
251 rows = rawindexentries(ui, repos, req, subdir=subdir)
251 rows = rawindexentries(ui, repos, req, subdir=subdir)
252
252
253 sortdefault = None, False
253 sortdefault = None, False
254
254
255 if sortcolumn and sortdefault != (sortcolumn, descending):
255 if sortcolumn and sortdefault != (sortcolumn, descending):
256 sortkey = b'%s_sort' % sortcolumn
256 sortkey = b'%s_sort' % sortcolumn
257 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
257 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
258
258
259 for row, parity in zip(rows, paritygen(stripecount)):
259 for row, parity in zip(rows, paritygen(stripecount)):
260 row[b'parity'] = parity
260 row[b'parity'] = parity
261 yield row
261 yield row
262
262
263
263
264 def indexentries(
264 def indexentries(
265 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
265 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
266 ):
266 ):
267 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
267 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
268 return templateutil.mappinggenerator(_indexentriesgen, args=args)
268 return templateutil.mappinggenerator(_indexentriesgen, args=args)
269
269
270
270
271 class hgwebdir:
271 class hgwebdir:
272 """HTTP server for multiple repositories.
272 """HTTP server for multiple repositories.
273
273
274 Given a configuration, different repositories will be served depending
274 Given a configuration, different repositories will be served depending
275 on the request path.
275 on the request path.
276
276
277 Instances are typically used as WSGI applications.
277 Instances are typically used as WSGI applications.
278 """
278 """
279
279
280 def __init__(self, conf, baseui=None):
280 def __init__(self, conf, baseui=None):
281 self.conf = conf
281 self.conf = conf
282 self.baseui = baseui
282 self.baseui = baseui
283 self.ui = None
283 self.ui = None
284 self.lastrefresh = 0
284 self.lastrefresh = 0
285 self.motd = None
285 self.motd = None
286 self.refresh()
286 self.refresh()
287 self.requests_count = 0
287 self.requests_count = 0
288 if not baseui:
288 if not baseui:
289 # set up environment for new ui
289 # set up environment for new ui
290 extensions.loadall(self.ui)
290 extensions.loadall(self.ui)
291 extensions.populateui(self.ui)
291 extensions.populateui(self.ui)
292
292
293 def refresh(self):
293 def refresh(self):
294 if self.ui:
294 if self.ui:
295 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
295 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
296 else:
296 else:
297 item = configitems.coreitems[b'web'][b'refreshinterval']
297 item = configitems.coreitems[b'web'][b'refreshinterval']
298 refreshinterval = item.default
298 refreshinterval = item.default
299
299
300 # refreshinterval <= 0 means to always refresh.
300 # refreshinterval <= 0 means to always refresh.
301 if (
301 if (
302 refreshinterval > 0
302 refreshinterval > 0
303 and self.lastrefresh + refreshinterval > time.time()
303 and self.lastrefresh + refreshinterval > time.time()
304 ):
304 ):
305 return
305 return
306
306
307 if self.baseui:
307 if self.baseui:
308 u = self.baseui.copy()
308 u = self.baseui.copy()
309 else:
309 else:
310 u = uimod.ui.load()
310 u = uimod.ui.load()
311 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
311 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
312 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
312 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
313 # displaying bundling progress bar while serving feels wrong and may
313 # displaying bundling progress bar while serving feels wrong and may
314 # break some wsgi implementations.
314 # break some wsgi implementations.
315 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
315 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
316
316
317 if not isinstance(self.conf, (dict, list, tuple)):
317 if not isinstance(self.conf, (dict, list, tuple)):
318 map = {b'paths': b'hgweb-paths'}
318 map = {b'paths': b'hgweb-paths'}
319 if not os.path.exists(self.conf):
319 if not os.path.exists(self.conf):
320 raise error.Abort(_(b'config file %s not found!') % self.conf)
320 raise error.Abort(_(b'config file %s not found!') % self.conf)
321 u.readconfig(self.conf, remap=map, trust=True)
321 u.readconfig(self.conf, remap=map, trust=True)
322 paths = []
322 paths = []
323 for name, ignored in u.configitems(b'hgweb-paths'):
323 for name, ignored in u.configitems(b'hgweb-paths'):
324 for path in u.configlist(b'hgweb-paths', name):
324 for path in u.configlist(b'hgweb-paths', name):
325 paths.append((name, path))
325 paths.append((name, path))
326 elif isinstance(self.conf, (list, tuple)):
326 elif isinstance(self.conf, (list, tuple)):
327 paths = self.conf
327 paths = self.conf
328 elif isinstance(self.conf, dict):
328 elif isinstance(self.conf, dict):
329 paths = self.conf.items()
329 paths = self.conf.items()
330 extensions.populateui(u)
330 extensions.populateui(u)
331
331
332 repos = findrepos(paths)
332 repos = findrepos(paths)
333 for prefix, root in u.configitems(b'collections'):
333 for prefix, root in u.configitems(b'collections'):
334 prefix = util.pconvert(prefix)
334 prefix = util.pconvert(prefix)
335 for path in scmutil.walkrepos(root, followsym=True):
335 for path in scmutil.walkrepos(root, followsym=True):
336 repo = os.path.normpath(path)
336 repo = os.path.normpath(path)
337 name = util.pconvert(repo)
337 name = util.pconvert(repo)
338 if name.startswith(prefix):
338 if name.startswith(prefix):
339 name = name[len(prefix) :]
339 name = name[len(prefix) :]
340 repos.append((name.lstrip(b'/'), repo))
340 repos.append((name.lstrip(b'/'), repo))
341
341
342 self.repos = repos
342 self.repos = repos
343 self.ui = u
343 self.ui = u
344 self.gc_full_collect_rate = self.ui.configint(
344 self.gc_full_collect_rate = self.ui.configint(
345 b'experimental', b'web.full-garbage-collection-rate'
345 b'experimental', b'web.full-garbage-collection-rate'
346 )
346 )
347 self.gc_full_collections_done = 0
347 self.gc_full_collections_done = 0
348 encoding.encoding = self.ui.config(b'web', b'encoding')
348 encoding.encoding = self.ui.config(b'web', b'encoding')
349 self.style = self.ui.config(b'web', b'style')
349 self.style = self.ui.config(b'web', b'style')
350 self.templatepath = self.ui.config(
350 self.templatepath = self.ui.config(
351 b'web', b'templates', untrusted=False
351 b'web', b'templates', untrusted=False
352 )
352 )
353 self.stripecount = self.ui.config(b'web', b'stripes')
353 self.stripecount = self.ui.config(b'web', b'stripes')
354 if self.stripecount:
354 if self.stripecount:
355 self.stripecount = int(self.stripecount)
355 self.stripecount = int(self.stripecount)
356 prefix = self.ui.config(b'web', b'prefix')
356 prefix = self.ui.config(b'web', b'prefix')
357 if prefix.startswith(b'/'):
357 if prefix.startswith(b'/'):
358 prefix = prefix[1:]
358 prefix = prefix[1:]
359 if prefix.endswith(b'/'):
359 if prefix.endswith(b'/'):
360 prefix = prefix[:-1]
360 prefix = prefix[:-1]
361 self.prefix = prefix
361 self.prefix = prefix
362 self.lastrefresh = time.time()
362 self.lastrefresh = time.time()
363
363
364 def run(self):
364 def run(self):
365 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
365 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
366 b"CGI/1."
366 b"CGI/1."
367 ):
367 ):
368 raise RuntimeError(
368 raise RuntimeError(
369 b"This function is only intended to be "
369 b"This function is only intended to be "
370 b"called while running as a CGI script."
370 b"called while running as a CGI script."
371 )
371 )
372 wsgicgi.launch(self)
372 wsgicgi.launch(self)
373
373
374 def __call__(self, env, respond):
374 def __call__(self, env, respond):
375 baseurl = self.ui.config(b'web', b'baseurl')
375 baseurl = self.ui.config(b'web', b'baseurl')
376 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
376 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
377 res = requestmod.wsgiresponse(req, respond)
377 res = requestmod.wsgiresponse(req, respond)
378
378
379 return self.run_wsgi(req, res)
379 return self.run_wsgi(req, res)
380
380
381 def run_wsgi(self, req, res):
381 def run_wsgi(self, req, res):
382 profile = self.ui.configbool(b'profiling', b'enabled')
382 profile = self.ui.configbool(b'profiling', b'enabled')
383 with profiling.profile(self.ui, enabled=profile):
383 with profiling.profile(self.ui, enabled=profile):
384 try:
384 try:
385 for r in self._runwsgi(req, res):
385 for r in self._runwsgi(req, res):
386 yield r
386 yield r
387 finally:
387 finally:
388 # There are known cycles in localrepository that prevent
388 # There are known cycles in localrepository that prevent
389 # those objects (and tons of held references) from being
389 # those objects (and tons of held references) from being
390 # collected through normal refcounting.
390 # collected through normal refcounting.
391 # In some cases, the resulting memory consumption can
391 # In some cases, the resulting memory consumption can
392 # be tamed by performing explicit garbage collections.
392 # be tamed by performing explicit garbage collections.
393 # In presence of actual leaks or big long-lived caches, the
393 # In presence of actual leaks or big long-lived caches, the
394 # impact on performance of such collections can become a
394 # impact on performance of such collections can become a
395 # problem, hence the rate shouldn't be set too low.
395 # problem, hence the rate shouldn't be set too low.
396 # See "Collecting the oldest generation" in
396 # See "Collecting the oldest generation" in
397 # https://devguide.python.org/garbage_collector
397 # https://devguide.python.org/garbage_collector
398 # for more about such trade-offs.
398 # for more about such trade-offs.
399 rate = self.gc_full_collect_rate
399 rate = self.gc_full_collect_rate
400
400
401 # this is not thread safe, but the consequence (skipping
401 # this is not thread safe, but the consequence (skipping
402 # a garbage collection) is arguably better than risking
402 # a garbage collection) is arguably better than risking
403 # to have several threads perform a collection in parallel
403 # to have several threads perform a collection in parallel
404 # (long useless wait on all threads).
404 # (long useless wait on all threads).
405 self.requests_count += 1
405 self.requests_count += 1
406 if rate > 0 and self.requests_count % rate == 0:
406 if rate > 0 and self.requests_count % rate == 0:
407 gc.collect()
407 gc.collect()
408 self.gc_full_collections_done += 1
408 self.gc_full_collections_done += 1
409 else:
409 else:
410 gc.collect(generation=1)
410 gc.collect(generation=1)
411
411
412 def _runwsgi(self, req, res):
412 def _runwsgi(self, req, res):
413 try:
413 self.refresh()
414 self.refresh()
415
414
416 csp, nonce = cspvalues(self.ui)
415 csp, nonce = cspvalues(self.ui)
417 if csp:
416 if csp:
418 res.headers[b'Content-Security-Policy'] = csp
417 res.headers[b'Content-Security-Policy'] = csp
419
418
420 virtual = req.dispatchpath.strip(b'/')
419 virtual = req.dispatchpath.strip(b'/')
421 tmpl = self.templater(req, nonce)
420 tmpl = self.templater(req, nonce)
421 try:
422 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
422 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
423
423
424 # Global defaults. These can be overridden by any handler.
424 # Global defaults. These can be overridden by any handler.
425 res.status = b'200 Script output follows'
425 res.status = b'200 Script output follows'
426 res.headers[b'Content-Type'] = ctype
426 res.headers[b'Content-Type'] = ctype
427
427
428 # a static file
428 # a static file
429 if virtual.startswith(b'static/') or b'static' in req.qsparams:
429 if virtual.startswith(b'static/') or b'static' in req.qsparams:
430 if virtual.startswith(b'static/'):
430 if virtual.startswith(b'static/'):
431 fname = virtual[7:]
431 fname = virtual[7:]
432 else:
432 else:
433 fname = req.qsparams[b'static']
433 fname = req.qsparams[b'static']
434 static = self.ui.config(b"web", b"static", untrusted=False)
434 static = self.ui.config(b"web", b"static", untrusted=False)
435 staticfile(self.templatepath, static, fname, res)
435 staticfile(self.templatepath, static, fname, res)
436 return res.sendresponse()
436 return res.sendresponse()
437
437
438 # top-level index
438 # top-level index
439
439
440 repos = dict(self.repos)
440 repos = dict(self.repos)
441
441
442 if (not virtual or virtual == b'index') and virtual not in repos:
442 if (not virtual or virtual == b'index') and virtual not in repos:
443 return self.makeindex(req, res, tmpl)
443 return self.makeindex(req, res, tmpl)
444
444
445 # nested indexes and hgwebs
445 # nested indexes and hgwebs
446
446
447 if virtual.endswith(b'/index') and virtual not in repos:
447 if virtual.endswith(b'/index') and virtual not in repos:
448 subdir = virtual[: -len(b'index')]
448 subdir = virtual[: -len(b'index')]
449 if any(r.startswith(subdir) for r in repos):
449 if any(r.startswith(subdir) for r in repos):
450 return self.makeindex(req, res, tmpl, subdir)
450 return self.makeindex(req, res, tmpl, subdir)
451
451
452 def _virtualdirs():
452 def _virtualdirs():
453 # Check the full virtual path, and each parent
453 # Check the full virtual path, and each parent
454 yield virtual
454 yield virtual
455 for p in pathutil.finddirs(virtual):
455 for p in pathutil.finddirs(virtual):
456 yield p
456 yield p
457
457
458 for virtualrepo in _virtualdirs():
458 for virtualrepo in _virtualdirs():
459 real = repos.get(virtualrepo)
459 real = repos.get(virtualrepo)
460 if real:
460 if real:
461 # Re-parse the WSGI environment to take into account our
461 # Re-parse the WSGI environment to take into account our
462 # repository path component.
462 # repository path component.
463 uenv = {
463 uenv = {
464 k.decode('latin1'): v for k, v in req.rawenv.items()
464 k.decode('latin1'): v for k, v in req.rawenv.items()
465 }
465 }
466 req = requestmod.parserequestfromenv(
466 req = requestmod.parserequestfromenv(
467 uenv,
467 uenv,
468 reponame=virtualrepo,
468 reponame=virtualrepo,
469 altbaseurl=self.ui.config(b'web', b'baseurl'),
469 altbaseurl=self.ui.config(b'web', b'baseurl'),
470 # Reuse wrapped body file object otherwise state
470 # Reuse wrapped body file object otherwise state
471 # tracking can get confused.
471 # tracking can get confused.
472 bodyfh=req.bodyfh,
472 bodyfh=req.bodyfh,
473 )
473 )
474 try:
474 try:
475 # ensure caller gets private copy of ui
475 # ensure caller gets private copy of ui
476 repo = hg.repository(self.ui.copy(), real)
476 repo = hg.repository(self.ui.copy(), real)
477 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
477 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
478 except IOError as inst:
478 except IOError as inst:
479 msg = encoding.strtolocal(inst.strerror)
479 msg = encoding.strtolocal(inst.strerror)
480 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
480 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
481 except error.RepoError as inst:
481 except error.RepoError as inst:
482 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
482 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
483
483
484 # browse subdirectories
484 # browse subdirectories
485 subdir = virtual + b'/'
485 subdir = virtual + b'/'
486 if [r for r in repos if r.startswith(subdir)]:
486 if [r for r in repos if r.startswith(subdir)]:
487 return self.makeindex(req, res, tmpl, subdir)
487 return self.makeindex(req, res, tmpl, subdir)
488
488
489 # prefixes not found
489 # prefixes not found
490 res.status = b'404 Not Found'
490 res.status = b'404 Not Found'
491 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
491 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
492 return res.sendresponse()
492 return res.sendresponse()
493
493
494 except ErrorResponse as e:
494 except ErrorResponse as e:
495 res.status = statusmessage(e.code, pycompat.bytestr(e))
495 res.status = statusmessage(e.code, pycompat.bytestr(e))
496 res.setbodygen(
496 res.setbodygen(
497 tmpl.generate(b'error', {b'error': e.message or b''})
497 tmpl.generate(b'error', {b'error': e.message or b''})
498 )
498 )
499 return res.sendresponse()
499 return res.sendresponse()
500 finally:
500 finally:
501 del tmpl
501 del tmpl
502
502
503 def makeindex(self, req, res, tmpl, subdir=b""):
503 def makeindex(self, req, res, tmpl, subdir=b""):
504 self.refresh()
504 self.refresh()
505 sortable = [b"name", b"description", b"contact", b"lastchange"]
505 sortable = [b"name", b"description", b"contact", b"lastchange"]
506 sortcolumn, descending = None, False
506 sortcolumn, descending = None, False
507 if b'sort' in req.qsparams:
507 if b'sort' in req.qsparams:
508 sortcolumn = req.qsparams[b'sort']
508 sortcolumn = req.qsparams[b'sort']
509 descending = sortcolumn.startswith(b'-')
509 descending = sortcolumn.startswith(b'-')
510 if descending:
510 if descending:
511 sortcolumn = sortcolumn[1:]
511 sortcolumn = sortcolumn[1:]
512 if sortcolumn not in sortable:
512 if sortcolumn not in sortable:
513 sortcolumn = b""
513 sortcolumn = b""
514
514
515 sort = [
515 sort = [
516 (
516 (
517 b"sort_%s" % column,
517 b"sort_%s" % column,
518 b"%s%s"
518 b"%s%s"
519 % (
519 % (
520 (not descending and column == sortcolumn) and b"-" or b"",
520 (not descending and column == sortcolumn) and b"-" or b"",
521 column,
521 column,
522 ),
522 ),
523 )
523 )
524 for column in sortable
524 for column in sortable
525 ]
525 ]
526
526
527 self.refresh()
527 self.refresh()
528
528
529 entries = indexentries(
529 entries = indexentries(
530 self.ui,
530 self.ui,
531 self.repos,
531 self.repos,
532 req,
532 req,
533 self.stripecount,
533 self.stripecount,
534 sortcolumn=sortcolumn,
534 sortcolumn=sortcolumn,
535 descending=descending,
535 descending=descending,
536 subdir=subdir,
536 subdir=subdir,
537 )
537 )
538
538
539 mapping = {
539 mapping = {
540 b'entries': entries,
540 b'entries': entries,
541 b'subdir': subdir,
541 b'subdir': subdir,
542 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
542 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
543 b'sortcolumn': sortcolumn,
543 b'sortcolumn': sortcolumn,
544 b'descending': descending,
544 b'descending': descending,
545 }
545 }
546 mapping.update(sort)
546 mapping.update(sort)
547 res.setbodygen(tmpl.generate(b'index', mapping))
547 res.setbodygen(tmpl.generate(b'index', mapping))
548 return res.sendresponse()
548 return res.sendresponse()
549
549
550 def templater(self, req, nonce):
550 def templater(self, req, nonce):
551 def config(*args, **kwargs):
551 def config(*args, **kwargs):
552 kwargs.setdefault('untrusted', True)
552 kwargs.setdefault('untrusted', True)
553 return self.ui.config(*args, **kwargs)
553 return self.ui.config(*args, **kwargs)
554
554
555 vars = {}
555 vars = {}
556 styles, (style, mapfile, fp) = hgweb_mod.getstyle(
556 styles, (style, mapfile, fp) = hgweb_mod.getstyle(
557 req, config, self.templatepath
557 req, config, self.templatepath
558 )
558 )
559 if style == styles[0]:
559 if style == styles[0]:
560 vars[b'style'] = style
560 vars[b'style'] = style
561
561
562 sessionvars = webutil.sessionvars(vars, b'?')
562 sessionvars = webutil.sessionvars(vars, b'?')
563 logourl = config(b'web', b'logourl')
563 logourl = config(b'web', b'logourl')
564 logoimg = config(b'web', b'logoimg')
564 logoimg = config(b'web', b'logoimg')
565 staticurl = (
565 staticurl = (
566 config(b'web', b'staticurl')
566 config(b'web', b'staticurl')
567 or req.apppath.rstrip(b'/') + b'/static/'
567 or req.apppath.rstrip(b'/') + b'/static/'
568 )
568 )
569 if not staticurl.endswith(b'/'):
569 if not staticurl.endswith(b'/'):
570 staticurl += b'/'
570 staticurl += b'/'
571
571
572 defaults = {
572 defaults = {
573 b"encoding": encoding.encoding,
573 b"encoding": encoding.encoding,
574 b"url": req.apppath + b'/',
574 b"url": req.apppath + b'/',
575 b"logourl": logourl,
575 b"logourl": logourl,
576 b"logoimg": logoimg,
576 b"logoimg": logoimg,
577 b"staticurl": staticurl,
577 b"staticurl": staticurl,
578 b"sessionvars": sessionvars,
578 b"sessionvars": sessionvars,
579 b"style": style,
579 b"style": style,
580 b"nonce": nonce,
580 b"nonce": nonce,
581 }
581 }
582 templatekeyword = registrar.templatekeyword(defaults)
582 templatekeyword = registrar.templatekeyword(defaults)
583
583
584 @templatekeyword(b'motd', requires=())
584 @templatekeyword(b'motd', requires=())
585 def motd(context, mapping):
585 def motd(context, mapping):
586 if self.motd is not None:
586 if self.motd is not None:
587 yield self.motd
587 yield self.motd
588 else:
588 else:
589 yield config(b'web', b'motd')
589 yield config(b'web', b'motd')
590
590
591 return templater.templater.frommapfile(
591 return templater.templater.frommapfile(
592 mapfile, fp=fp, defaults=defaults
592 mapfile, fp=fp, defaults=defaults
593 )
593 )
General Comments 0
You need to be logged in to leave comments. Login now