##// END OF EJS Templates
py3: remove compat module usage
super-admin -
r1038:ae22fbad python3
parent child Browse files
Show More
@@ -1,693 +1,693 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 28 from io import StringIO
29 29
30 30 import simplejson as json
31 31 import msgpack
32 import configparser
32 33 from pyramid.config import Configurator
33 34 from pyramid.settings import asbool, aslist
34 35 from pyramid.wsgi import wsgiapp
35 from pyramid.compat import configparser
36 36 from pyramid.response import Response
37 37
38 38 from vcsserver.utils import safe_int
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
43 43 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
44 44
45 45 try:
46 46 locale.setlocale(locale.LC_ALL, '')
47 47 except locale.Error as e:
48 48 log.error(
49 49 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
50 50 os.environ['LC_ALL'] = 'C'
51 51
52 52 import vcsserver
53 53 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
54 54 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
55 55 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
56 56 from vcsserver.echo_stub.echo_app import EchoApp
57 57 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
58 58 from vcsserver.lib.exc_tracking import store_exception
59 59 from vcsserver.server import VcsServer
60 60
61 61
62 62 git_import_err = None
63 63 try:
64 64 from vcsserver.git import GitFactory, GitRemote
65 65 except ImportError as e:
66 66 GitFactory = None
67 67 GitRemote = None
68 68 git_import_err = e
69 69
70 70 hg_import_err = None
71 71 try:
72 72 from vcsserver.hg import MercurialFactory, HgRemote
73 73 except ImportError as e:
74 74 MercurialFactory = None
75 75 HgRemote = None
76 76 hg_import_err = e
77 77
78 78 svn_import_err = None
79 79 try:
80 80 from vcsserver.svn import SubversionFactory, SvnRemote
81 81 except ImportError as e:
82 82 SubversionFactory = None
83 83 SvnRemote = None
84 84 svn_import_err = e
85 85
86 86
87 87 def _is_request_chunked(environ):
88 88 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
89 89 return stream
90 90
91 91
92 92 def _int_setting(settings, name, default):
93 93 settings[name] = int(settings.get(name, default))
94 94 return settings[name]
95 95
96 96
97 97 def _bool_setting(settings, name, default):
98 98 input_val = settings.get(name, default)
99 99 if isinstance(input_val, str):
100 100 input_val = input_val.encode('utf8')
101 101 settings[name] = asbool(input_val)
102 102 return settings[name]
103 103
104 104
105 105 def _list_setting(settings, name, default):
106 106 raw_value = settings.get(name, default)
107 107
108 108 # Otherwise we assume it uses pyramids space/newline separation.
109 109 settings[name] = aslist(raw_value)
110 110 return settings[name]
111 111
112 112
113 113 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
114 114 value = settings.get(name, default)
115 115
116 116 if default_when_empty and not value:
117 117 # use default value when value is empty
118 118 value = default
119 119
120 120 if lower:
121 121 value = value.lower()
122 122 settings[name] = value
123 123 return settings[name]
124 124
125 125
126 126 class VCS(object):
127 127 def __init__(self, locale_conf=None, cache_config=None):
128 128 self.locale = locale_conf
129 129 self.cache_config = cache_config
130 130 self._configure_locale()
131 131
132 132 if GitFactory and GitRemote:
133 133 git_factory = GitFactory()
134 134 self._git_remote = GitRemote(git_factory)
135 135 else:
136 136 log.error("Git client import failed: %s", git_import_err)
137 137
138 138 if MercurialFactory and HgRemote:
139 139 hg_factory = MercurialFactory()
140 140 self._hg_remote = HgRemote(hg_factory)
141 141 else:
142 142 log.error("Mercurial client import failed: %s", hg_import_err)
143 143
144 144 if SubversionFactory and SvnRemote:
145 145 svn_factory = SubversionFactory()
146 146
147 147 # hg factory is used for svn url validation
148 148 hg_factory = MercurialFactory()
149 149 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
150 150 else:
151 151 log.error("Subversion client import failed: %s", svn_import_err)
152 152
153 153 self._vcsserver = VcsServer()
154 154
155 155 def _configure_locale(self):
156 156 if self.locale:
157 157 log.info('Settings locale: `LC_ALL` to %s', self.locale)
158 158 else:
159 159 log.info('Configuring locale subsystem based on environment variables')
160 160 try:
161 161 # If self.locale is the empty string, then the locale
162 162 # module will use the environment variables. See the
163 163 # documentation of the package `locale`.
164 164 locale.setlocale(locale.LC_ALL, self.locale)
165 165
166 166 language_code, encoding = locale.getlocale()
167 167 log.info(
168 168 'Locale set to language code "%s" with encoding "%s".',
169 169 language_code, encoding)
170 170 except locale.Error:
171 171 log.exception('Cannot set locale, not configuring the locale system')
172 172
173 173
174 174 class WsgiProxy(object):
175 175 def __init__(self, wsgi):
176 176 self.wsgi = wsgi
177 177
178 178 def __call__(self, environ, start_response):
179 179 input_data = environ['wsgi.input'].read()
180 180 input_data = msgpack.unpackb(input_data)
181 181
182 182 error = None
183 183 try:
184 184 data, status, headers = self.wsgi.handle(
185 185 input_data['environment'], input_data['input_data'],
186 186 *input_data['args'], **input_data['kwargs'])
187 187 except Exception as e:
188 188 data, status, headers = [], None, None
189 189 error = {
190 190 'message': str(e),
191 191 '_vcs_kind': getattr(e, '_vcs_kind', None)
192 192 }
193 193
194 194 start_response(200, {})
195 195 return self._iterator(error, status, headers, data)
196 196
197 197 def _iterator(self, error, status, headers, data):
198 198 initial_data = [
199 199 error,
200 200 status,
201 201 headers,
202 202 ]
203 203
204 204 for d in chain(initial_data, data):
205 205 yield msgpack.packb(d)
206 206
207 207
208 208 def not_found(request):
209 209 return {'status': '404 NOT FOUND'}
210 210
211 211
212 212 class VCSViewPredicate(object):
213 213 def __init__(self, val, config):
214 214 self.remotes = val
215 215
216 216 def text(self):
217 217 return 'vcs view method = %s' % (list(self.remotes.keys()),)
218 218
219 219 phash = text
220 220
221 221 def __call__(self, context, request):
222 222 """
223 223 View predicate that returns true if given backend is supported by
224 224 defined remotes.
225 225 """
226 226 backend = request.matchdict.get('backend')
227 227 return backend in self.remotes
228 228
229 229
230 230 class HTTPApplication(object):
231 231 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
232 232
233 233 remote_wsgi = remote_wsgi
234 234 _use_echo_app = False
235 235
236 236 def __init__(self, settings=None, global_config=None):
237 237 self._sanitize_settings_and_apply_defaults(settings)
238 238
239 239 self.config = Configurator(settings=settings)
240 240 self.global_config = global_config
241 241 self.config.include('vcsserver.lib.rc_cache')
242 242
243 243 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
244 244 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
245 245 self._remotes = {
246 246 'hg': vcs._hg_remote,
247 247 'git': vcs._git_remote,
248 248 'svn': vcs._svn_remote,
249 249 'server': vcs._vcsserver,
250 250 }
251 251 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
252 252 self._use_echo_app = True
253 253 log.warning("Using EchoApp for VCS operations.")
254 254 self.remote_wsgi = remote_wsgi_stub
255 255
256 256 self._configure_settings(global_config, settings)
257 257 self._configure()
258 258
259 259 def _configure_settings(self, global_config, app_settings):
260 260 """
261 261 Configure the settings module.
262 262 """
263 263 settings_merged = global_config.copy()
264 264 settings_merged.update(app_settings)
265 265
266 266 git_path = app_settings.get('git_path', None)
267 267 if git_path:
268 268 settings.GIT_EXECUTABLE = git_path
269 269 binary_dir = app_settings.get('core.binary_dir', None)
270 270 if binary_dir:
271 271 settings.BINARY_DIR = binary_dir
272 272
273 273 # Store the settings to make them available to other modules.
274 274 vcsserver.PYRAMID_SETTINGS = settings_merged
275 275 vcsserver.CONFIG = settings_merged
276 276
277 277 def _sanitize_settings_and_apply_defaults(self, settings):
278 278 temp_store = tempfile.gettempdir()
279 279 default_cache_dir = os.path.join(temp_store, 'rc_cache')
280 280
281 281 # save default, cache dir, and use it for all backends later.
282 282 default_cache_dir = _string_setting(
283 283 settings,
284 284 'cache_dir',
285 285 default_cache_dir, lower=False, default_when_empty=True)
286 286
287 287 # ensure we have our dir created
288 288 if not os.path.isdir(default_cache_dir):
289 289 os.makedirs(default_cache_dir, mode=0o755)
290 290
291 291 # exception store cache
292 292 _string_setting(
293 293 settings,
294 294 'exception_tracker.store_path',
295 295 temp_store, lower=False, default_when_empty=True)
296 296
297 297 # repo_object cache
298 298 _string_setting(
299 299 settings,
300 300 'rc_cache.repo_object.backend',
301 301 'dogpile.cache.rc.file_namespace', lower=False)
302 302 _int_setting(
303 303 settings,
304 304 'rc_cache.repo_object.expiration_time',
305 305 30 * 24 * 60 * 60)
306 306 _string_setting(
307 307 settings,
308 308 'rc_cache.repo_object.arguments.filename',
309 309 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
310 310
311 311 def _configure(self):
312 312 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
313 313
314 314 self.config.add_route('service', '/_service')
315 315 self.config.add_route('status', '/status')
316 316 self.config.add_route('hg_proxy', '/proxy/hg')
317 317 self.config.add_route('git_proxy', '/proxy/git')
318 318
319 319 # rpc methods
320 320 self.config.add_route('vcs', '/{backend}')
321 321
322 322 # streaming rpc remote methods
323 323 self.config.add_route('vcs_stream', '/{backend}/stream')
324 324
325 325 # vcs operations clone/push as streaming
326 326 self.config.add_route('stream_git', '/stream/git/*repo_name')
327 327 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
328 328
329 329 self.config.add_view(self.status_view, route_name='status', renderer='json')
330 330 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
331 331
332 332 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
333 333 self.config.add_view(self.git_proxy(), route_name='git_proxy')
334 334 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
335 335 vcs_view=self._remotes)
336 336 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
337 337 vcs_view=self._remotes)
338 338
339 339 self.config.add_view(self.hg_stream(), route_name='stream_hg')
340 340 self.config.add_view(self.git_stream(), route_name='stream_git')
341 341
342 342 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
343 343
344 344 self.config.add_notfound_view(not_found, renderer='json')
345 345
346 346 self.config.add_view(self.handle_vcs_exception, context=Exception)
347 347
348 348 self.config.add_tween(
349 349 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
350 350 )
351 351 self.config.add_request_method(
352 352 'vcsserver.lib.request_counter.get_request_counter',
353 353 'request_count')
354 354
355 355 def wsgi_app(self):
356 356 return self.config.make_wsgi_app()
357 357
358 358 def _vcs_view_params(self, request):
359 359 remote = self._remotes[request.matchdict['backend']]
360 360 payload = msgpack.unpackb(request.body, use_list=True)
361 361 method = payload.get('method')
362 362 params = payload['params']
363 363 wire = params.get('wire')
364 364 args = params.get('args')
365 365 kwargs = params.get('kwargs')
366 366 context_uid = None
367 367
368 368 if wire:
369 369 try:
370 370 wire['context'] = context_uid = uuid.UUID(wire['context'])
371 371 except KeyError:
372 372 pass
373 373 args.insert(0, wire)
374 374 repo_state_uid = wire.get('repo_state_uid') if wire else None
375 375
376 376 # NOTE(marcink): trading complexity for slight performance
377 377 if log.isEnabledFor(logging.DEBUG):
378 378 no_args_methods = [
379 379 'archive_repo'
380 380 ]
381 381 if method in no_args_methods:
382 382 call_args = ''
383 383 else:
384 384 call_args = args[1:]
385 385
386 386 log.debug('method requested:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
387 387 method, call_args, kwargs, context_uid, repo_state_uid)
388 388
389 389 return payload, remote, method, args, kwargs
390 390
391 391 def vcs_view(self, request):
392 392
393 393 payload, remote, method, args, kwargs = self._vcs_view_params(request)
394 394 payload_id = payload.get('id')
395 395
396 396 try:
397 397 resp = getattr(remote, method)(*args, **kwargs)
398 398 except Exception as e:
399 399 exc_info = list(sys.exc_info())
400 400 exc_type, exc_value, exc_traceback = exc_info
401 401
402 402 org_exc = getattr(e, '_org_exc', None)
403 403 org_exc_name = None
404 404 org_exc_tb = ''
405 405 if org_exc:
406 406 org_exc_name = org_exc.__class__.__name__
407 407 org_exc_tb = getattr(e, '_org_exc_tb', '')
408 408 # replace our "faked" exception with our org
409 409 exc_info[0] = org_exc.__class__
410 410 exc_info[1] = org_exc
411 411
412 412 should_store_exc = True
413 413 if org_exc:
414 414 def get_exc_fqn(_exc_obj):
415 415 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
416 416 return module_name + '.' + org_exc_name
417 417
418 418 exc_fqn = get_exc_fqn(org_exc)
419 419
420 420 if exc_fqn in ['mercurial.error.RepoLookupError',
421 421 'vcsserver.exceptions.RefNotFoundException']:
422 422 should_store_exc = False
423 423
424 424 if should_store_exc:
425 425 store_exception(id(exc_info), exc_info)
426 426
427 427 tb_info = ''.join(
428 428 traceback.format_exception(exc_type, exc_value, exc_traceback))
429 429
430 430 type_ = e.__class__.__name__
431 431 if type_ not in self.ALLOWED_EXCEPTIONS:
432 432 type_ = None
433 433
434 434 resp = {
435 435 'id': payload_id,
436 436 'error': {
437 437 'message': e.message,
438 438 'traceback': tb_info,
439 439 'org_exc': org_exc_name,
440 440 'org_exc_tb': org_exc_tb,
441 441 'type': type_
442 442 }
443 443 }
444 444 try:
445 445 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
446 446 except AttributeError:
447 447 pass
448 448 else:
449 449 resp = {
450 450 'id': payload_id,
451 451 'result': resp
452 452 }
453 453
454 454 return resp
455 455
456 456 def vcs_stream_view(self, request):
457 457 payload, remote, method, args, kwargs = self._vcs_view_params(request)
458 458 # this method has a stream: marker we remove it here
459 459 method = method.split('stream:')[-1]
460 460 chunk_size = safe_int(payload.get('chunk_size')) or 4096
461 461
462 462 try:
463 463 resp = getattr(remote, method)(*args, **kwargs)
464 464 except Exception as e:
465 465 raise
466 466
467 467 def get_chunked_data(method_resp):
468 468 stream = StringIO(method_resp)
469 469 while 1:
470 470 chunk = stream.read(chunk_size)
471 471 if not chunk:
472 472 break
473 473 yield chunk
474 474
475 475 response = Response(app_iter=get_chunked_data(resp))
476 476 response.content_type = 'application/octet-stream'
477 477
478 478 return response
479 479
480 480 def status_view(self, request):
481 481 import vcsserver
482 482 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
483 483 'pid': os.getpid()}
484 484
485 485 def service_view(self, request):
486 486 import vcsserver
487 487
488 488 payload = msgpack.unpackb(request.body, use_list=True)
489 489 server_config, app_config = {}, {}
490 490
491 491 try:
492 492 path = self.global_config['__file__']
493 493 config = configparser.RawConfigParser()
494 494
495 495 config.read(path)
496 496
497 497 if config.has_section('server:main'):
498 498 server_config = dict(config.items('server:main'))
499 499 if config.has_section('app:main'):
500 500 app_config = dict(config.items('app:main'))
501 501
502 502 except Exception:
503 503 log.exception('Failed to read .ini file for display')
504 504
505 505 environ = list(os.environ.items())
506 506
507 507 resp = {
508 508 'id': payload.get('id'),
509 509 'result': dict(
510 510 version=vcsserver.__version__,
511 511 config=server_config,
512 512 app_config=app_config,
513 513 environ=environ,
514 514 payload=payload,
515 515 )
516 516 }
517 517 return resp
518 518
519 519 def _msgpack_renderer_factory(self, info):
520 520 def _render(value, system):
521 521 request = system.get('request')
522 522 if request is not None:
523 523 response = request.response
524 524 ct = response.content_type
525 525 if ct == response.default_content_type:
526 526 response.content_type = 'application/x-msgpack'
527 527 return msgpack.packb(value)
528 528 return _render
529 529
530 530 def set_env_from_config(self, environ, config):
531 531 dict_conf = {}
532 532 try:
533 533 for elem in config:
534 534 if elem[0] == 'rhodecode':
535 535 dict_conf = json.loads(elem[2])
536 536 break
537 537 except Exception:
538 538 log.exception('Failed to fetch SCM CONFIG')
539 539 return
540 540
541 541 username = dict_conf.get('username')
542 542 if username:
543 543 environ['REMOTE_USER'] = username
544 544 # mercurial specific, some extension api rely on this
545 545 environ['HGUSER'] = username
546 546
547 547 ip = dict_conf.get('ip')
548 548 if ip:
549 549 environ['REMOTE_HOST'] = ip
550 550
551 551 if _is_request_chunked(environ):
552 552 # set the compatibility flag for webob
553 553 environ['wsgi.input_terminated'] = True
554 554
555 555 def hg_proxy(self):
556 556 @wsgiapp
557 557 def _hg_proxy(environ, start_response):
558 558 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
559 559 return app(environ, start_response)
560 560 return _hg_proxy
561 561
562 562 def git_proxy(self):
563 563 @wsgiapp
564 564 def _git_proxy(environ, start_response):
565 565 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
566 566 return app(environ, start_response)
567 567 return _git_proxy
568 568
569 569 def hg_stream(self):
570 570 if self._use_echo_app:
571 571 @wsgiapp
572 572 def _hg_stream(environ, start_response):
573 573 app = EchoApp('fake_path', 'fake_name', None)
574 574 return app(environ, start_response)
575 575 return _hg_stream
576 576 else:
577 577 @wsgiapp
578 578 def _hg_stream(environ, start_response):
579 579 log.debug('http-app: handling hg stream')
580 580 repo_path = environ['HTTP_X_RC_REPO_PATH']
581 581 repo_name = environ['HTTP_X_RC_REPO_NAME']
582 582 packed_config = base64.b64decode(
583 583 environ['HTTP_X_RC_REPO_CONFIG'])
584 584 config = msgpack.unpackb(packed_config)
585 585 app = scm_app.create_hg_wsgi_app(
586 586 repo_path, repo_name, config)
587 587
588 588 # Consistent path information for hgweb
589 589 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
590 590 environ['REPO_NAME'] = repo_name
591 591 self.set_env_from_config(environ, config)
592 592
593 593 log.debug('http-app: starting app handler '
594 594 'with %s and process request', app)
595 595 return app(environ, ResponseFilter(start_response))
596 596 return _hg_stream
597 597
598 598 def git_stream(self):
599 599 if self._use_echo_app:
600 600 @wsgiapp
601 601 def _git_stream(environ, start_response):
602 602 app = EchoApp('fake_path', 'fake_name', None)
603 603 return app(environ, start_response)
604 604 return _git_stream
605 605 else:
606 606 @wsgiapp
607 607 def _git_stream(environ, start_response):
608 608 log.debug('http-app: handling git stream')
609 609 repo_path = environ['HTTP_X_RC_REPO_PATH']
610 610 repo_name = environ['HTTP_X_RC_REPO_NAME']
611 611 packed_config = base64.b64decode(
612 612 environ['HTTP_X_RC_REPO_CONFIG'])
613 613 config = msgpack.unpackb(packed_config)
614 614
615 615 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
616 616 self.set_env_from_config(environ, config)
617 617
618 618 content_type = environ.get('CONTENT_TYPE', '')
619 619
620 620 path = environ['PATH_INFO']
621 621 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
622 622 log.debug(
623 623 'LFS: Detecting if request `%s` is LFS server path based '
624 624 'on content type:`%s`, is_lfs:%s',
625 625 path, content_type, is_lfs_request)
626 626
627 627 if not is_lfs_request:
628 628 # fallback detection by path
629 629 if GIT_LFS_PROTO_PAT.match(path):
630 630 is_lfs_request = True
631 631 log.debug(
632 632 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
633 633 path, is_lfs_request)
634 634
635 635 if is_lfs_request:
636 636 app = scm_app.create_git_lfs_wsgi_app(
637 637 repo_path, repo_name, config)
638 638 else:
639 639 app = scm_app.create_git_wsgi_app(
640 640 repo_path, repo_name, config)
641 641
642 642 log.debug('http-app: starting app handler '
643 643 'with %s and process request', app)
644 644
645 645 return app(environ, start_response)
646 646
647 647 return _git_stream
648 648
649 649 def handle_vcs_exception(self, exception, request):
650 650 _vcs_kind = getattr(exception, '_vcs_kind', '')
651 651 if _vcs_kind == 'repo_locked':
652 652 # Get custom repo-locked status code if present.
653 653 status_code = request.headers.get('X-RC-Locked-Status-Code')
654 654 return HTTPRepoLocked(
655 655 title=exception.message, status_code=status_code)
656 656
657 657 elif _vcs_kind == 'repo_branch_protected':
658 658 # Get custom repo-branch-protected status code if present.
659 659 return HTTPRepoBranchProtected(title=exception.message)
660 660
661 661 exc_info = request.exc_info
662 662 store_exception(id(exc_info), exc_info)
663 663
664 664 traceback_info = 'unavailable'
665 665 if request.exc_info:
666 666 exc_type, exc_value, exc_tb = request.exc_info
667 667 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
668 668
669 669 log.error(
670 670 'error occurred handling this request for path: %s, \n tb: %s',
671 671 request.path, traceback_info)
672 672 raise exception
673 673
674 674
675 675 class ResponseFilter(object):
676 676
677 677 def __init__(self, start_response):
678 678 self._start_response = start_response
679 679
680 680 def __call__(self, status, response_headers, exc_info=None):
681 681 headers = tuple(
682 682 (h, v) for h, v in response_headers
683 683 if not wsgiref.util.is_hop_by_hop(h))
684 684 return self._start_response(status, headers, exc_info)
685 685
686 686
687 687 def main(global_config, **settings):
688 688 if MercurialFactory:
689 689 hgpatches.patch_largefiles_capabilities()
690 690 hgpatches.patch_subrepo_type_mapping()
691 691
692 692 app = HTTPApplication(settings=settings, global_config=global_config)
693 693 return app.wsgi_app()
@@ -1,169 +1,167 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2020 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20
21 21 import os
22 22 import time
23 23 import datetime
24 24 import msgpack
25 25 import logging
26 26 import traceback
27 27 import tempfile
28 28
29 from pyramid import compat
30
31 29 log = logging.getLogger(__name__)
32 30
33 31 # NOTE: Any changes should be synced with exc_tracking at rhodecode.lib.exc_tracking
34 32 global_prefix = 'vcsserver'
35 33 exc_store_dir_name = 'rc_exception_store_v1'
36 34
37 35
38 36 def exc_serialize(exc_id, tb, exc_type):
39 37
40 38 data = {
41 39 'version': 'v1',
42 40 'exc_id': exc_id,
43 41 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
44 42 'exc_timestamp': repr(time.time()),
45 43 'exc_message': tb,
46 44 'exc_type': exc_type,
47 45 }
48 46 return msgpack.packb(data), data
49 47
50 48
51 49 def exc_unserialize(tb):
52 50 return msgpack.unpackb(tb)
53 51
54 52
55 53 def get_exc_store():
56 54 """
57 55 Get and create exception store if it's not existing
58 56 """
59 57 import vcsserver as app
60 58
61 59 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
62 60 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
63 61
64 62 _exc_store_path = os.path.abspath(_exc_store_path)
65 63 if not os.path.isdir(_exc_store_path):
66 64 os.makedirs(_exc_store_path)
67 65 log.debug('Initializing exceptions store at %s', _exc_store_path)
68 66 return _exc_store_path
69 67
70 68
71 69 def _store_exception(exc_id, exc_info, prefix):
72 70 exc_type, exc_value, exc_traceback = exc_info
73 71
74 72 tb = ''.join(traceback.format_exception(
75 73 exc_type, exc_value, exc_traceback, None))
76 74
77 75 detailed_tb = getattr(exc_value, '_org_exc_tb', None)
78 76
79 77 if detailed_tb:
80 if isinstance(detailed_tb, compat.string_types):
78 if isinstance(detailed_tb, str):
81 79 remote_tb = [detailed_tb]
82 80
83 81 tb += (
84 82 '\n+++ BEG SOURCE EXCEPTION +++\n\n'
85 83 '{}\n'
86 84 '+++ END SOURCE EXCEPTION +++\n'
87 85 ''.format('\n'.join(remote_tb))
88 86 )
89 87
90 88 # Avoid that remote_tb also appears in the frame
91 89 del remote_tb
92 90
93 91 exc_type_name = exc_type.__name__
94 92 exc_store_path = get_exc_store()
95 93 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
96 94 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
97 95 if not os.path.isdir(exc_store_path):
98 96 os.makedirs(exc_store_path)
99 97 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
100 98 with open(stored_exc_path, 'wb') as f:
101 99 f.write(exc_data)
102 100 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
103 101
104 102
105 103 def store_exception(exc_id, exc_info, prefix=global_prefix):
106 104 """
107 105 Example usage::
108 106
109 107 exc_info = sys.exc_info()
110 108 store_exception(id(exc_info), exc_info)
111 109 """
112 110
113 111 try:
114 112 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
115 113 except Exception:
116 114 log.exception('Failed to store exception `%s` information', exc_id)
117 115 # there's no way this can fail, it will crash server badly if it does.
118 116 pass
119 117
120 118
121 119 def _find_exc_file(exc_id, prefix=global_prefix):
122 120 exc_store_path = get_exc_store()
123 121 if prefix:
124 122 exc_id = '{}_{}'.format(exc_id, prefix)
125 123 else:
126 124 # search without a prefix
127 125 exc_id = '{}'.format(exc_id)
128 126
129 127 # we need to search the store for such start pattern as above
130 128 for fname in os.listdir(exc_store_path):
131 129 if fname.startswith(exc_id):
132 130 exc_id = os.path.join(exc_store_path, fname)
133 131 break
134 132 continue
135 133 else:
136 134 exc_id = None
137 135
138 136 return exc_id
139 137
140 138
141 139 def _read_exception(exc_id, prefix):
142 140 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
143 141 if exc_id_file_path:
144 142 with open(exc_id_file_path, 'rb') as f:
145 143 return exc_unserialize(f.read())
146 144 else:
147 145 log.debug('Exception File `%s` not found', exc_id_file_path)
148 146 return None
149 147
150 148
151 149 def read_exception(exc_id, prefix=global_prefix):
152 150 try:
153 151 return _read_exception(exc_id=exc_id, prefix=prefix)
154 152 except Exception:
155 153 log.exception('Failed to read exception `%s` information', exc_id)
156 154 # there's no way this can fail, it will crash server badly if it does.
157 155 return None
158 156
159 157
160 158 def delete_exception(exc_id, prefix=global_prefix):
161 159 try:
162 160 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
163 161 if exc_id_file_path:
164 162 os.remove(exc_id_file_path)
165 163
166 164 except Exception:
167 165 log.exception('Failed to remove exception `%s` information', exc_id)
168 166 # there's no way this can fail, it will crash server badly if it does.
169 167 pass
@@ -1,253 +1,253 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 time
19 19 import errno
20 20 import logging
21 21
22 22 import msgpack
23 23 import redis
24 24
25 25 from dogpile.cache.api import CachedValue
26 26 from dogpile.cache.backends import memory as memory_backend
27 27 from dogpile.cache.backends import file as file_backend
28 28 from dogpile.cache.backends import redis as redis_backend
29 from dogpile.cache.backends.file import NO_VALUE, compat, FileLock
29 from dogpile.cache.backends.file import NO_VALUE, FileLock
30 30 from dogpile.cache.util import memoized_property
31 31
32 32 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
33 33
34 34
35 35 _default_max_size = 1024
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class LRUMemoryBackend(memory_backend.MemoryBackend):
41 41 key_prefix = 'lru_mem_backend'
42 42 pickle_values = False
43 43
44 44 def __init__(self, arguments):
45 45 max_size = arguments.pop('max_size', _default_max_size)
46 46
47 47 LRUDictClass = LRUDict
48 48 if arguments.pop('log_key_count', None):
49 49 LRUDictClass = LRUDictDebug
50 50
51 51 arguments['cache_dict'] = LRUDictClass(max_size)
52 52 super(LRUMemoryBackend, self).__init__(arguments)
53 53
54 54 def delete(self, key):
55 55 try:
56 56 del self._cache[key]
57 57 except KeyError:
58 58 # we don't care if key isn't there at deletion
59 59 pass
60 60
61 61 def delete_multi(self, keys):
62 62 for key in keys:
63 63 self.delete(key)
64 64
65 65
66 66 class PickleSerializer(object):
67 67
68 68 def _dumps(self, value, safe=False):
69 69 try:
70 return compat.pickle.dumps(value)
70 return pickle.dumps(value)
71 71 except Exception:
72 72 if safe:
73 73 return NO_VALUE
74 74 else:
75 75 raise
76 76
77 77 def _loads(self, value, safe=True):
78 78 try:
79 return compat.pickle.loads(value)
79 return pickle.loads(value)
80 80 except Exception:
81 81 if safe:
82 82 return NO_VALUE
83 83 else:
84 84 raise
85 85
86 86
87 87 class MsgPackSerializer(object):
88 88
89 89 def _dumps(self, value, safe=False):
90 90 try:
91 91 return msgpack.packb(value)
92 92 except Exception:
93 93 if safe:
94 94 return NO_VALUE
95 95 else:
96 96 raise
97 97
98 98 def _loads(self, value, safe=True):
99 99 """
100 100 pickle maintained the `CachedValue` wrapper of the tuple
101 101 msgpack does not, so it must be added back in.
102 102 """
103 103 try:
104 104 value = msgpack.unpackb(value, use_list=False)
105 105 return CachedValue(*value)
106 106 except Exception:
107 107 if safe:
108 108 return NO_VALUE
109 109 else:
110 110 raise
111 111
112 112
113 113 import fcntl
114 114 flock_org = fcntl.flock
115 115
116 116
117 117 class CustomLockFactory(FileLock):
118 118
119 119 pass
120 120
121 121
122 122 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
123 123 key_prefix = 'file_backend'
124 124
125 125 def __init__(self, arguments):
126 126 arguments['lock_factory'] = CustomLockFactory
127 127 super(FileNamespaceBackend, self).__init__(arguments)
128 128
129 129 def __repr__(self):
130 130 return '{} `{}`'.format(self.__class__, self.filename)
131 131
132 132 def list_keys(self, prefix=''):
133 133 prefix = '{}:{}'.format(self.key_prefix, prefix)
134 134
135 135 def cond(v):
136 136 if not prefix:
137 137 return True
138 138
139 139 if v.startswith(prefix):
140 140 return True
141 141 return False
142 142
143 143 with self._dbm_file(True) as dbm:
144 144
145 145 return filter(cond, dbm.keys())
146 146
147 147 def get_store(self):
148 148 return self.filename
149 149
150 150 def get(self, key):
151 151 with self._dbm_file(False) as dbm:
152 152 if hasattr(dbm, 'get'):
153 153 value = dbm.get(key, NO_VALUE)
154 154 else:
155 155 # gdbm objects lack a .get method
156 156 try:
157 157 value = dbm[key]
158 158 except KeyError:
159 159 value = NO_VALUE
160 160 if value is not NO_VALUE:
161 161 value = self._loads(value)
162 162 return value
163 163
164 164 def set(self, key, value):
165 165 with self._dbm_file(True) as dbm:
166 166 dbm[key] = self._dumps(value)
167 167
168 168 def set_multi(self, mapping):
169 169 with self._dbm_file(True) as dbm:
170 170 for key, value in mapping.items():
171 171 dbm[key] = self._dumps(value)
172 172
173 173
174 174 class BaseRedisBackend(redis_backend.RedisBackend):
175 175
176 176 def _create_client(self):
177 177 args = {}
178 178
179 179 if self.url is not None:
180 180 args.update(url=self.url)
181 181
182 182 else:
183 183 args.update(
184 184 host=self.host, password=self.password,
185 185 port=self.port, db=self.db
186 186 )
187 187
188 188 connection_pool = redis.ConnectionPool(**args)
189 189
190 190 return redis.StrictRedis(connection_pool=connection_pool)
191 191
192 192 def list_keys(self, prefix=''):
193 193 prefix = '{}:{}*'.format(self.key_prefix, prefix)
194 194 return self.client.keys(prefix)
195 195
196 196 def get_store(self):
197 197 return self.client.connection_pool
198 198
199 199 def get(self, key):
200 200 value = self.client.get(key)
201 201 if value is None:
202 202 return NO_VALUE
203 203 return self._loads(value)
204 204
205 205 def get_multi(self, keys):
206 206 if not keys:
207 207 return []
208 208 values = self.client.mget(keys)
209 209 loads = self._loads
210 210 return [
211 211 loads(v) if v is not None else NO_VALUE
212 212 for v in values]
213 213
214 214 def set(self, key, value):
215 215 if self.redis_expiration_time:
216 216 self.client.setex(key, self.redis_expiration_time,
217 217 self._dumps(value))
218 218 else:
219 219 self.client.set(key, self._dumps(value))
220 220
221 221 def set_multi(self, mapping):
222 222 dumps = self._dumps
223 223 mapping = dict(
224 224 (k, dumps(v))
225 225 for k, v in mapping.items()
226 226 )
227 227
228 228 if not self.redis_expiration_time:
229 229 self.client.mset(mapping)
230 230 else:
231 231 pipe = self.client.pipeline()
232 232 for key, value in mapping.items():
233 233 pipe.setex(key, self.redis_expiration_time, value)
234 234 pipe.execute()
235 235
236 236 def get_mutex(self, key):
237 237 u = redis_backend.u
238 238 if self.distributed_lock:
239 239 lock_key = u('_lock_{0}').format(key)
240 240 log.debug('Trying to acquire Redis lock for key %s', lock_key)
241 241 return self.client.lock(lock_key, self.lock_timeout, self.lock_sleep)
242 242 else:
243 243 return None
244 244
245 245
246 246 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
247 247 key_prefix = 'redis_pickle_backend'
248 248 pass
249 249
250 250
251 251 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
252 252 key_prefix = 'redis_msgpack_backend'
253 253 pass
General Comments 0
You need to be logged in to leave comments. Login now