##// END OF EJS Templates
errors: use a better interface to track exceptions and tracebacks.
marcink -
r478:439940e9 default
parent child Browse files
Show More
@@ -1,487 +1,490 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import base64
20 20 import locale
21 21 import logging
22 22 import uuid
23 23 import wsgiref.util
24 24 import traceback
25 25 from itertools import chain
26 26
27 27 import simplejson as json
28 28 import msgpack
29 29 from beaker.cache import CacheManager
30 30 from beaker.util import parse_cache_config_options
31 31 from pyramid.config import Configurator
32 32 from pyramid.wsgi import wsgiapp
33 33 from pyramid.compat import configparser
34 34
35 35 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
36 36 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
37 37 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
38 38 from vcsserver.echo_stub.echo_app import EchoApp
39 39 from vcsserver.exceptions import HTTPRepoLocked
40 40 from vcsserver.server import VcsServer
41 41
42 42 try:
43 43 from vcsserver.git import GitFactory, GitRemote
44 44 except ImportError:
45 45 GitFactory = None
46 46 GitRemote = None
47 47
48 48 try:
49 49 from vcsserver.hg import MercurialFactory, HgRemote
50 50 except ImportError:
51 51 MercurialFactory = None
52 52 HgRemote = None
53 53
54 54 try:
55 55 from vcsserver.svn import SubversionFactory, SvnRemote
56 56 except ImportError:
57 57 SubversionFactory = None
58 58 SvnRemote = None
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 def _is_request_chunked(environ):
64 64 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
65 65 return stream
66 66
67 67
68 68 class VCS(object):
69 69 def __init__(self, locale=None, cache_config=None):
70 70 self.locale = locale
71 71 self.cache_config = cache_config
72 72 self._configure_locale()
73 73 self._initialize_cache()
74 74
75 75 if GitFactory and GitRemote:
76 76 git_repo_cache = self.cache.get_cache_region(
77 77 'git', region='repo_object')
78 78 git_factory = GitFactory(git_repo_cache)
79 79 self._git_remote = GitRemote(git_factory)
80 80 else:
81 81 log.info("Git client import failed")
82 82
83 83 if MercurialFactory and HgRemote:
84 84 hg_repo_cache = self.cache.get_cache_region(
85 85 'hg', region='repo_object')
86 86 hg_factory = MercurialFactory(hg_repo_cache)
87 87 self._hg_remote = HgRemote(hg_factory)
88 88 else:
89 89 log.info("Mercurial client import failed")
90 90
91 91 if SubversionFactory and SvnRemote:
92 92 svn_repo_cache = self.cache.get_cache_region(
93 93 'svn', region='repo_object')
94 94 svn_factory = SubversionFactory(svn_repo_cache)
95 95 # hg factory is used for svn url validation
96 96 hg_repo_cache = self.cache.get_cache_region(
97 97 'hg', region='repo_object')
98 98 hg_factory = MercurialFactory(hg_repo_cache)
99 99 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
100 100 else:
101 101 log.info("Subversion client import failed")
102 102
103 103 self._vcsserver = VcsServer()
104 104
105 105 def _initialize_cache(self):
106 106 cache_config = parse_cache_config_options(self.cache_config)
107 107 log.info('Initializing beaker cache: %s' % cache_config)
108 108 self.cache = CacheManager(**cache_config)
109 109
110 110 def _configure_locale(self):
111 111 if self.locale:
112 112 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
113 113 else:
114 114 log.info(
115 115 'Configuring locale subsystem based on environment variables')
116 116 try:
117 117 # If self.locale is the empty string, then the locale
118 118 # module will use the environment variables. See the
119 119 # documentation of the package `locale`.
120 120 locale.setlocale(locale.LC_ALL, self.locale)
121 121
122 122 language_code, encoding = locale.getlocale()
123 123 log.info(
124 124 'Locale set to language code "%s" with encoding "%s".',
125 125 language_code, encoding)
126 126 except locale.Error:
127 127 log.exception(
128 128 'Cannot set locale, not configuring the locale system')
129 129
130 130
131 131 class WsgiProxy(object):
132 132 def __init__(self, wsgi):
133 133 self.wsgi = wsgi
134 134
135 135 def __call__(self, environ, start_response):
136 136 input_data = environ['wsgi.input'].read()
137 137 input_data = msgpack.unpackb(input_data)
138 138
139 139 error = None
140 140 try:
141 141 data, status, headers = self.wsgi.handle(
142 142 input_data['environment'], input_data['input_data'],
143 143 *input_data['args'], **input_data['kwargs'])
144 144 except Exception as e:
145 145 data, status, headers = [], None, None
146 146 error = {
147 147 'message': str(e),
148 148 '_vcs_kind': getattr(e, '_vcs_kind', None)
149 149 }
150 150
151 151 start_response(200, {})
152 152 return self._iterator(error, status, headers, data)
153 153
154 154 def _iterator(self, error, status, headers, data):
155 155 initial_data = [
156 156 error,
157 157 status,
158 158 headers,
159 159 ]
160 160
161 161 for d in chain(initial_data, data):
162 162 yield msgpack.packb(d)
163 163
164 164
165 165 class HTTPApplication(object):
166 166 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
167 167
168 168 remote_wsgi = remote_wsgi
169 169 _use_echo_app = False
170 170
171 171 def __init__(self, settings=None, global_config=None):
172 172 self.config = Configurator(settings=settings)
173 173 self.global_config = global_config
174 174
175 175 locale = settings.get('locale', '') or 'en_US.UTF-8'
176 176 vcs = VCS(locale=locale, cache_config=settings)
177 177 self._remotes = {
178 178 'hg': vcs._hg_remote,
179 179 'git': vcs._git_remote,
180 180 'svn': vcs._svn_remote,
181 181 'server': vcs._vcsserver,
182 182 }
183 183 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
184 184 self._use_echo_app = True
185 185 log.warning("Using EchoApp for VCS operations.")
186 186 self.remote_wsgi = remote_wsgi_stub
187 187 self._configure_settings(settings)
188 188 self._configure()
189 189
190 190 def _configure_settings(self, app_settings):
191 191 """
192 192 Configure the settings module.
193 193 """
194 194 git_path = app_settings.get('git_path', None)
195 195 if git_path:
196 196 settings.GIT_EXECUTABLE = git_path
197 197 binary_dir = app_settings.get('core.binary_dir', None)
198 198 if binary_dir:
199 199 settings.BINARY_DIR = binary_dir
200 200
201 201 def _configure(self):
202 202 self.config.add_renderer(
203 203 name='msgpack',
204 204 factory=self._msgpack_renderer_factory)
205 205
206 206 self.config.add_route('service', '/_service')
207 207 self.config.add_route('status', '/status')
208 208 self.config.add_route('hg_proxy', '/proxy/hg')
209 209 self.config.add_route('git_proxy', '/proxy/git')
210 210 self.config.add_route('vcs', '/{backend}')
211 211 self.config.add_route('stream_git', '/stream/git/*repo_name')
212 212 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
213 213
214 214 self.config.add_view(
215 215 self.status_view, route_name='status', renderer='json')
216 216 self.config.add_view(
217 217 self.service_view, route_name='service', renderer='msgpack')
218 218
219 219 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
220 220 self.config.add_view(self.git_proxy(), route_name='git_proxy')
221 221 self.config.add_view(
222 222 self.vcs_view, route_name='vcs', renderer='msgpack',
223 223 custom_predicates=[self.is_vcs_view])
224 224
225 225 self.config.add_view(self.hg_stream(), route_name='stream_hg')
226 226 self.config.add_view(self.git_stream(), route_name='stream_git')
227 227
228 228 def notfound(request):
229 229 return {'status': '404 NOT FOUND'}
230 230 self.config.add_notfound_view(notfound, renderer='json')
231 231
232 232 self.config.add_view(self.handle_vcs_exception, context=Exception)
233 233
234 234 self.config.add_tween(
235 235 'vcsserver.tweens.RequestWrapperTween',
236 236 )
237 237
238 238 def wsgi_app(self):
239 239 return self.config.make_wsgi_app()
240 240
241 241 def vcs_view(self, request):
242 242 remote = self._remotes[request.matchdict['backend']]
243 243 payload = msgpack.unpackb(request.body, use_list=True)
244 244 method = payload.get('method')
245 245 params = payload.get('params')
246 246 wire = params.get('wire')
247 247 args = params.get('args')
248 248 kwargs = params.get('kwargs')
249 249 if wire:
250 250 try:
251 251 wire['context'] = uuid.UUID(wire['context'])
252 252 except KeyError:
253 253 pass
254 254 args.insert(0, wire)
255 255
256 256 log.debug('method called:%s with kwargs:%s', method, kwargs)
257 257 try:
258 258 resp = getattr(remote, method)(*args, **kwargs)
259 259 except Exception as e:
260 260 tb_info = traceback.format_exc()
261 261
262 262 type_ = e.__class__.__name__
263 263 if type_ not in self.ALLOWED_EXCEPTIONS:
264 264 type_ = None
265 265
266 266 resp = {
267 267 'id': payload.get('id'),
268 268 'error': {
269 269 'message': e.message,
270 270 'traceback': tb_info,
271 271 'type': type_
272 272 }
273 273 }
274 274 try:
275 275 resp['error']['_vcs_kind'] = e._vcs_kind
276 276 except AttributeError:
277 277 pass
278 278 else:
279 279 resp = {
280 280 'id': payload.get('id'),
281 281 'result': resp
282 282 }
283 283
284 284 return resp
285 285
286 286 def status_view(self, request):
287 287 import vcsserver
288 288 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
289 289 'pid': os.getpid()}
290 290
291 291 def service_view(self, request):
292 292 import vcsserver
293 293
294 294 payload = msgpack.unpackb(request.body, use_list=True)
295 295
296 296 try:
297 297 path = self.global_config['__file__']
298 298 config = configparser.ConfigParser()
299 299 config.read(path)
300 300 parsed_ini = config
301 301 if parsed_ini.has_section('server:main'):
302 302 parsed_ini = dict(parsed_ini.items('server:main'))
303 303 except Exception:
304 304 log.exception('Failed to read .ini file for display')
305 305 parsed_ini = {}
306 306
307 307 resp = {
308 308 'id': payload.get('id'),
309 309 'result': dict(
310 310 version=vcsserver.__version__,
311 311 config=parsed_ini,
312 312 payload=payload,
313 313 )
314 314 }
315 315 return resp
316 316
317 317 def _msgpack_renderer_factory(self, info):
318 318 def _render(value, system):
319 319 value = msgpack.packb(value)
320 320 request = system.get('request')
321 321 if request is not None:
322 322 response = request.response
323 323 ct = response.content_type
324 324 if ct == response.default_content_type:
325 325 response.content_type = 'application/x-msgpack'
326 326 return value
327 327 return _render
328 328
329 329 def set_env_from_config(self, environ, config):
330 330 dict_conf = {}
331 331 try:
332 332 for elem in config:
333 333 if elem[0] == 'rhodecode':
334 334 dict_conf = json.loads(elem[2])
335 335 break
336 336 except Exception:
337 337 log.exception('Failed to fetch SCM CONFIG')
338 338 return
339 339
340 340 username = dict_conf.get('username')
341 341 if username:
342 342 environ['REMOTE_USER'] = username
343 343 # mercurial specific, some extension api rely on this
344 344 environ['HGUSER'] = username
345 345
346 346 ip = dict_conf.get('ip')
347 347 if ip:
348 348 environ['REMOTE_HOST'] = ip
349 349
350 350 if _is_request_chunked(environ):
351 351 # set the compatibility flag for webob
352 352 environ['wsgi.input_terminated'] = True
353 353
354 354 def hg_proxy(self):
355 355 @wsgiapp
356 356 def _hg_proxy(environ, start_response):
357 357 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
358 358 return app(environ, start_response)
359 359 return _hg_proxy
360 360
361 361 def git_proxy(self):
362 362 @wsgiapp
363 363 def _git_proxy(environ, start_response):
364 364 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
365 365 return app(environ, start_response)
366 366 return _git_proxy
367 367
368 368 def hg_stream(self):
369 369 if self._use_echo_app:
370 370 @wsgiapp
371 371 def _hg_stream(environ, start_response):
372 372 app = EchoApp('fake_path', 'fake_name', None)
373 373 return app(environ, start_response)
374 374 return _hg_stream
375 375 else:
376 376 @wsgiapp
377 377 def _hg_stream(environ, start_response):
378 378 log.debug('http-app: handling hg stream')
379 379 repo_path = environ['HTTP_X_RC_REPO_PATH']
380 380 repo_name = environ['HTTP_X_RC_REPO_NAME']
381 381 packed_config = base64.b64decode(
382 382 environ['HTTP_X_RC_REPO_CONFIG'])
383 383 config = msgpack.unpackb(packed_config)
384 384 app = scm_app.create_hg_wsgi_app(
385 385 repo_path, repo_name, config)
386 386
387 387 # Consistent path information for hgweb
388 388 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
389 389 environ['REPO_NAME'] = repo_name
390 390 self.set_env_from_config(environ, config)
391 391
392 392 log.debug('http-app: starting app handler '
393 393 'with %s and process request', app)
394 394 return app(environ, ResponseFilter(start_response))
395 395 return _hg_stream
396 396
397 397 def git_stream(self):
398 398 if self._use_echo_app:
399 399 @wsgiapp
400 400 def _git_stream(environ, start_response):
401 401 app = EchoApp('fake_path', 'fake_name', None)
402 402 return app(environ, start_response)
403 403 return _git_stream
404 404 else:
405 405 @wsgiapp
406 406 def _git_stream(environ, start_response):
407 407 log.debug('http-app: handling git stream')
408 408 repo_path = environ['HTTP_X_RC_REPO_PATH']
409 409 repo_name = environ['HTTP_X_RC_REPO_NAME']
410 410 packed_config = base64.b64decode(
411 411 environ['HTTP_X_RC_REPO_CONFIG'])
412 412 config = msgpack.unpackb(packed_config)
413 413
414 414 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
415 415 self.set_env_from_config(environ, config)
416 416
417 417 content_type = environ.get('CONTENT_TYPE', '')
418 418
419 419 path = environ['PATH_INFO']
420 420 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
421 421 log.debug(
422 422 'LFS: Detecting if request `%s` is LFS server path based '
423 423 'on content type:`%s`, is_lfs:%s',
424 424 path, content_type, is_lfs_request)
425 425
426 426 if not is_lfs_request:
427 427 # fallback detection by path
428 428 if GIT_LFS_PROTO_PAT.match(path):
429 429 is_lfs_request = True
430 430 log.debug(
431 431 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
432 432 path, is_lfs_request)
433 433
434 434 if is_lfs_request:
435 435 app = scm_app.create_git_lfs_wsgi_app(
436 436 repo_path, repo_name, config)
437 437 else:
438 438 app = scm_app.create_git_wsgi_app(
439 439 repo_path, repo_name, config)
440 440
441 441 log.debug('http-app: starting app handler '
442 442 'with %s and process request', app)
443 443
444 444 return app(environ, start_response)
445 445
446 446 return _git_stream
447 447
448 448 def is_vcs_view(self, context, request):
449 449 """
450 450 View predicate that returns true if given backend is supported by
451 451 defined remotes.
452 452 """
453 453 backend = request.matchdict.get('backend')
454 454 return backend in self._remotes
455 455
456 456 def handle_vcs_exception(self, exception, request):
457 457 _vcs_kind = getattr(exception, '_vcs_kind', '')
458 458 if _vcs_kind == 'repo_locked':
459 459 # Get custom repo-locked status code if present.
460 460 status_code = request.headers.get('X-RC-Locked-Status-Code')
461 461 return HTTPRepoLocked(
462 462 title=exception.message, status_code=status_code)
463 traceback_info = 'unavailable'
464 if request.exc_info:
465 traceback_info = traceback.format_exc(request.exc_info[2])
463 466
464 # Re-raise exception if we can not handle it.
465 log.exception(
466 'error occurred handling this request for path: %s', request.path)
467 log.error(
468 'error occurred handling this request for path: %s, \n tb: %s',
469 request.path, traceback_info)
467 470 raise exception
468 471
469 472
470 473 class ResponseFilter(object):
471 474
472 475 def __init__(self, start_response):
473 476 self._start_response = start_response
474 477
475 478 def __call__(self, status, response_headers, exc_info=None):
476 479 headers = tuple(
477 480 (h, v) for h, v in response_headers
478 481 if not wsgiref.util.is_hop_by_hop(h))
479 482 return self._start_response(status, headers, exc_info)
480 483
481 484
482 485 def main(global_config, **settings):
483 486 if MercurialFactory:
484 487 hgpatches.patch_largefiles_capabilities()
485 488 hgpatches.patch_subrepo_type_mapping()
486 489 app = HTTPApplication(settings=settings, global_config=global_config)
487 490 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now