##// END OF EJS Templates
locale: use a hacky way to catch the locale set error. We rather "cleanup" the env ourselfs...
marcink -
r507:84e9d5ef default
parent child Browse files
Show More
@@ -1,545 +1,559 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 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 from itertools import chain
27 27
28 28 import simplejson as json
29 29 import msgpack
30 30 from pyramid.config import Configurator
31 31 from pyramid.settings import asbool, aslist
32 32 from pyramid.wsgi import wsgiapp
33 33 from pyramid.compat import configparser
34 34
35
36 log = logging.getLogger(__name__)
37
38 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
39 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
40
41 try:
42 locale.setlocale(locale.LC_ALL, '')
43 except locale.Error as e:
44 log.error(
45 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
46 os.environ['LC_ALL'] = 'C'
47
48
35 49 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
36 50 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
37 51 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
38 52 from vcsserver.echo_stub.echo_app import EchoApp
39 53 from vcsserver.exceptions import HTTPRepoLocked
40 54 from vcsserver.lib.exc_tracking import store_exception
41 55 from vcsserver.server import VcsServer
42 56
43 57 try:
44 58 from vcsserver.git import GitFactory, GitRemote
45 59 except ImportError:
46 60 GitFactory = None
47 61 GitRemote = None
48 62
49 63 try:
50 64 from vcsserver.hg import MercurialFactory, HgRemote
51 65 except ImportError:
52 66 MercurialFactory = None
53 67 HgRemote = None
54 68
55 69 try:
56 70 from vcsserver.svn import SubversionFactory, SvnRemote
57 71 except ImportError:
58 72 SubversionFactory = None
59 73 SvnRemote = None
60 74
61 log = logging.getLogger(__name__)
75
62 76
63 77
64 78 def _is_request_chunked(environ):
65 79 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
66 80 return stream
67 81
68 82
69 83 def _int_setting(settings, name, default):
70 84 settings[name] = int(settings.get(name, default))
71 85
72 86
73 87 def _bool_setting(settings, name, default):
74 88 input_val = settings.get(name, default)
75 89 if isinstance(input_val, unicode):
76 90 input_val = input_val.encode('utf8')
77 91 settings[name] = asbool(input_val)
78 92
79 93
80 94 def _list_setting(settings, name, default):
81 95 raw_value = settings.get(name, default)
82 96
83 97 # Otherwise we assume it uses pyramids space/newline separation.
84 98 settings[name] = aslist(raw_value)
85 99
86 100
87 101 def _string_setting(settings, name, default, lower=True):
88 102 value = settings.get(name, default)
89 103 if lower:
90 104 value = value.lower()
91 105 settings[name] = value
92 106
93 107
94 108 class VCS(object):
95 109 def __init__(self, locale=None, cache_config=None):
96 110 self.locale = locale
97 111 self.cache_config = cache_config
98 112 self._configure_locale()
99 113
100 114 if GitFactory and GitRemote:
101 115 git_factory = GitFactory()
102 116 self._git_remote = GitRemote(git_factory)
103 117 else:
104 118 log.info("Git client import failed")
105 119
106 120 if MercurialFactory and HgRemote:
107 121 hg_factory = MercurialFactory()
108 122 self._hg_remote = HgRemote(hg_factory)
109 123 else:
110 124 log.info("Mercurial client import failed")
111 125
112 126 if SubversionFactory and SvnRemote:
113 127 svn_factory = SubversionFactory()
114 128
115 129 # hg factory is used for svn url validation
116 130 hg_factory = MercurialFactory()
117 131 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
118 132 else:
119 133 log.info("Subversion client import failed")
120 134
121 135 self._vcsserver = VcsServer()
122 136
123 137 def _configure_locale(self):
124 138 if self.locale:
125 139 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
126 140 else:
127 141 log.info(
128 142 'Configuring locale subsystem based on environment variables')
129 143 try:
130 144 # If self.locale is the empty string, then the locale
131 145 # module will use the environment variables. See the
132 146 # documentation of the package `locale`.
133 147 locale.setlocale(locale.LC_ALL, self.locale)
134 148
135 149 language_code, encoding = locale.getlocale()
136 150 log.info(
137 151 'Locale set to language code "%s" with encoding "%s".',
138 152 language_code, encoding)
139 153 except locale.Error:
140 154 log.exception(
141 155 'Cannot set locale, not configuring the locale system')
142 156
143 157
144 158 class WsgiProxy(object):
145 159 def __init__(self, wsgi):
146 160 self.wsgi = wsgi
147 161
148 162 def __call__(self, environ, start_response):
149 163 input_data = environ['wsgi.input'].read()
150 164 input_data = msgpack.unpackb(input_data)
151 165
152 166 error = None
153 167 try:
154 168 data, status, headers = self.wsgi.handle(
155 169 input_data['environment'], input_data['input_data'],
156 170 *input_data['args'], **input_data['kwargs'])
157 171 except Exception as e:
158 172 data, status, headers = [], None, None
159 173 error = {
160 174 'message': str(e),
161 175 '_vcs_kind': getattr(e, '_vcs_kind', None)
162 176 }
163 177
164 178 start_response(200, {})
165 179 return self._iterator(error, status, headers, data)
166 180
167 181 def _iterator(self, error, status, headers, data):
168 182 initial_data = [
169 183 error,
170 184 status,
171 185 headers,
172 186 ]
173 187
174 188 for d in chain(initial_data, data):
175 189 yield msgpack.packb(d)
176 190
177 191
178 192 class HTTPApplication(object):
179 193 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
180 194
181 195 remote_wsgi = remote_wsgi
182 196 _use_echo_app = False
183 197
184 198 def __init__(self, settings=None, global_config=None):
185 199 self._sanitize_settings_and_apply_defaults(settings)
186 200
187 201 self.config = Configurator(settings=settings)
188 202 self.global_config = global_config
189 203 self.config.include('vcsserver.lib.rc_cache')
190 204
191 205 locale = settings.get('locale', '') or 'en_US.UTF-8'
192 206 vcs = VCS(locale=locale, cache_config=settings)
193 207 self._remotes = {
194 208 'hg': vcs._hg_remote,
195 209 'git': vcs._git_remote,
196 210 'svn': vcs._svn_remote,
197 211 'server': vcs._vcsserver,
198 212 }
199 213 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
200 214 self._use_echo_app = True
201 215 log.warning("Using EchoApp for VCS operations.")
202 216 self.remote_wsgi = remote_wsgi_stub
203 217 self._configure_settings(settings)
204 218 self._configure()
205 219
206 220 def _configure_settings(self, app_settings):
207 221 """
208 222 Configure the settings module.
209 223 """
210 224 git_path = app_settings.get('git_path', None)
211 225 if git_path:
212 226 settings.GIT_EXECUTABLE = git_path
213 227 binary_dir = app_settings.get('core.binary_dir', None)
214 228 if binary_dir:
215 229 settings.BINARY_DIR = binary_dir
216 230
217 231 def _sanitize_settings_and_apply_defaults(self, settings):
218 232 # repo_object cache
219 233 _string_setting(
220 234 settings,
221 235 'rc_cache.repo_object.backend',
222 236 'dogpile.cache.rc.memory_lru')
223 237 _int_setting(
224 238 settings,
225 239 'rc_cache.repo_object.expiration_time',
226 240 300)
227 241 _int_setting(
228 242 settings,
229 243 'rc_cache.repo_object.max_size',
230 244 1024)
231 245
232 246 def _configure(self):
233 247 self.config.add_renderer(
234 248 name='msgpack',
235 249 factory=self._msgpack_renderer_factory)
236 250
237 251 self.config.add_route('service', '/_service')
238 252 self.config.add_route('status', '/status')
239 253 self.config.add_route('hg_proxy', '/proxy/hg')
240 254 self.config.add_route('git_proxy', '/proxy/git')
241 255 self.config.add_route('vcs', '/{backend}')
242 256 self.config.add_route('stream_git', '/stream/git/*repo_name')
243 257 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
244 258
245 259 self.config.add_view(
246 260 self.status_view, route_name='status', renderer='json')
247 261 self.config.add_view(
248 262 self.service_view, route_name='service', renderer='msgpack')
249 263
250 264 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
251 265 self.config.add_view(self.git_proxy(), route_name='git_proxy')
252 266 self.config.add_view(
253 267 self.vcs_view, route_name='vcs', renderer='msgpack',
254 268 custom_predicates=[self.is_vcs_view])
255 269
256 270 self.config.add_view(self.hg_stream(), route_name='stream_hg')
257 271 self.config.add_view(self.git_stream(), route_name='stream_git')
258 272
259 273 def notfound(request):
260 274 return {'status': '404 NOT FOUND'}
261 275 self.config.add_notfound_view(notfound, renderer='json')
262 276
263 277 self.config.add_view(self.handle_vcs_exception, context=Exception)
264 278
265 279 self.config.add_tween(
266 280 'vcsserver.tweens.RequestWrapperTween',
267 281 )
268 282
269 283 def wsgi_app(self):
270 284 return self.config.make_wsgi_app()
271 285
272 286 def vcs_view(self, request):
273 287 remote = self._remotes[request.matchdict['backend']]
274 288 payload = msgpack.unpackb(request.body, use_list=True)
275 289 method = payload.get('method')
276 290 params = payload.get('params')
277 291 wire = params.get('wire')
278 292 args = params.get('args')
279 293 kwargs = params.get('kwargs')
280 294 context_uid = None
281 295
282 296 if wire:
283 297 try:
284 298 wire['context'] = context_uid = uuid.UUID(wire['context'])
285 299 except KeyError:
286 300 pass
287 301 args.insert(0, wire)
288 302
289 303 log.debug('method called:%s with kwargs:%s context_uid: %s',
290 304 method, kwargs, context_uid)
291 305 try:
292 306 resp = getattr(remote, method)(*args, **kwargs)
293 307 except Exception as e:
294 308 exc_info = list(sys.exc_info())
295 309 exc_type, exc_value, exc_traceback = exc_info
296 310
297 311 org_exc = getattr(e, '_org_exc', None)
298 312 org_exc_name = None
299 313 if org_exc:
300 314 org_exc_name = org_exc.__class__.__name__
301 315 # replace our "faked" exception with our org
302 316 exc_info[0] = org_exc.__class__
303 317 exc_info[1] = org_exc
304 318
305 319 store_exception(id(exc_info), exc_info)
306 320
307 321 tb_info = ''.join(
308 322 traceback.format_exception(exc_type, exc_value, exc_traceback))
309 323
310 324 type_ = e.__class__.__name__
311 325 if type_ not in self.ALLOWED_EXCEPTIONS:
312 326 type_ = None
313 327
314 328 resp = {
315 329 'id': payload.get('id'),
316 330 'error': {
317 331 'message': e.message,
318 332 'traceback': tb_info,
319 333 'org_exc': org_exc_name,
320 334 'type': type_
321 335 }
322 336 }
323 337 try:
324 338 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
325 339 except AttributeError:
326 340 pass
327 341 else:
328 342 resp = {
329 343 'id': payload.get('id'),
330 344 'result': resp
331 345 }
332 346
333 347 return resp
334 348
335 349 def status_view(self, request):
336 350 import vcsserver
337 351 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
338 352 'pid': os.getpid()}
339 353
340 354 def service_view(self, request):
341 355 import vcsserver
342 356
343 357 payload = msgpack.unpackb(request.body, use_list=True)
344 358
345 359 try:
346 360 path = self.global_config['__file__']
347 361 config = configparser.ConfigParser()
348 362 config.read(path)
349 363 parsed_ini = config
350 364 if parsed_ini.has_section('server:main'):
351 365 parsed_ini = dict(parsed_ini.items('server:main'))
352 366 except Exception:
353 367 log.exception('Failed to read .ini file for display')
354 368 parsed_ini = {}
355 369
356 370 resp = {
357 371 'id': payload.get('id'),
358 372 'result': dict(
359 373 version=vcsserver.__version__,
360 374 config=parsed_ini,
361 375 payload=payload,
362 376 )
363 377 }
364 378 return resp
365 379
366 380 def _msgpack_renderer_factory(self, info):
367 381 def _render(value, system):
368 382 value = msgpack.packb(value)
369 383 request = system.get('request')
370 384 if request is not None:
371 385 response = request.response
372 386 ct = response.content_type
373 387 if ct == response.default_content_type:
374 388 response.content_type = 'application/x-msgpack'
375 389 return value
376 390 return _render
377 391
378 392 def set_env_from_config(self, environ, config):
379 393 dict_conf = {}
380 394 try:
381 395 for elem in config:
382 396 if elem[0] == 'rhodecode':
383 397 dict_conf = json.loads(elem[2])
384 398 break
385 399 except Exception:
386 400 log.exception('Failed to fetch SCM CONFIG')
387 401 return
388 402
389 403 username = dict_conf.get('username')
390 404 if username:
391 405 environ['REMOTE_USER'] = username
392 406 # mercurial specific, some extension api rely on this
393 407 environ['HGUSER'] = username
394 408
395 409 ip = dict_conf.get('ip')
396 410 if ip:
397 411 environ['REMOTE_HOST'] = ip
398 412
399 413 if _is_request_chunked(environ):
400 414 # set the compatibility flag for webob
401 415 environ['wsgi.input_terminated'] = True
402 416
403 417 def hg_proxy(self):
404 418 @wsgiapp
405 419 def _hg_proxy(environ, start_response):
406 420 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
407 421 return app(environ, start_response)
408 422 return _hg_proxy
409 423
410 424 def git_proxy(self):
411 425 @wsgiapp
412 426 def _git_proxy(environ, start_response):
413 427 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
414 428 return app(environ, start_response)
415 429 return _git_proxy
416 430
417 431 def hg_stream(self):
418 432 if self._use_echo_app:
419 433 @wsgiapp
420 434 def _hg_stream(environ, start_response):
421 435 app = EchoApp('fake_path', 'fake_name', None)
422 436 return app(environ, start_response)
423 437 return _hg_stream
424 438 else:
425 439 @wsgiapp
426 440 def _hg_stream(environ, start_response):
427 441 log.debug('http-app: handling hg stream')
428 442 repo_path = environ['HTTP_X_RC_REPO_PATH']
429 443 repo_name = environ['HTTP_X_RC_REPO_NAME']
430 444 packed_config = base64.b64decode(
431 445 environ['HTTP_X_RC_REPO_CONFIG'])
432 446 config = msgpack.unpackb(packed_config)
433 447 app = scm_app.create_hg_wsgi_app(
434 448 repo_path, repo_name, config)
435 449
436 450 # Consistent path information for hgweb
437 451 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
438 452 environ['REPO_NAME'] = repo_name
439 453 self.set_env_from_config(environ, config)
440 454
441 455 log.debug('http-app: starting app handler '
442 456 'with %s and process request', app)
443 457 return app(environ, ResponseFilter(start_response))
444 458 return _hg_stream
445 459
446 460 def git_stream(self):
447 461 if self._use_echo_app:
448 462 @wsgiapp
449 463 def _git_stream(environ, start_response):
450 464 app = EchoApp('fake_path', 'fake_name', None)
451 465 return app(environ, start_response)
452 466 return _git_stream
453 467 else:
454 468 @wsgiapp
455 469 def _git_stream(environ, start_response):
456 470 log.debug('http-app: handling git stream')
457 471 repo_path = environ['HTTP_X_RC_REPO_PATH']
458 472 repo_name = environ['HTTP_X_RC_REPO_NAME']
459 473 packed_config = base64.b64decode(
460 474 environ['HTTP_X_RC_REPO_CONFIG'])
461 475 config = msgpack.unpackb(packed_config)
462 476
463 477 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
464 478 self.set_env_from_config(environ, config)
465 479
466 480 content_type = environ.get('CONTENT_TYPE', '')
467 481
468 482 path = environ['PATH_INFO']
469 483 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
470 484 log.debug(
471 485 'LFS: Detecting if request `%s` is LFS server path based '
472 486 'on content type:`%s`, is_lfs:%s',
473 487 path, content_type, is_lfs_request)
474 488
475 489 if not is_lfs_request:
476 490 # fallback detection by path
477 491 if GIT_LFS_PROTO_PAT.match(path):
478 492 is_lfs_request = True
479 493 log.debug(
480 494 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
481 495 path, is_lfs_request)
482 496
483 497 if is_lfs_request:
484 498 app = scm_app.create_git_lfs_wsgi_app(
485 499 repo_path, repo_name, config)
486 500 else:
487 501 app = scm_app.create_git_wsgi_app(
488 502 repo_path, repo_name, config)
489 503
490 504 log.debug('http-app: starting app handler '
491 505 'with %s and process request', app)
492 506
493 507 return app(environ, start_response)
494 508
495 509 return _git_stream
496 510
497 511 def is_vcs_view(self, context, request):
498 512 """
499 513 View predicate that returns true if given backend is supported by
500 514 defined remotes.
501 515 """
502 516 backend = request.matchdict.get('backend')
503 517 return backend in self._remotes
504 518
505 519 def handle_vcs_exception(self, exception, request):
506 520 _vcs_kind = getattr(exception, '_vcs_kind', '')
507 521 if _vcs_kind == 'repo_locked':
508 522 # Get custom repo-locked status code if present.
509 523 status_code = request.headers.get('X-RC-Locked-Status-Code')
510 524 return HTTPRepoLocked(
511 525 title=exception.message, status_code=status_code)
512 526
513 527 exc_info = request.exc_info
514 528 store_exception(id(exc_info), exc_info)
515 529
516 530 traceback_info = 'unavailable'
517 531 if request.exc_info:
518 532 exc_type, exc_value, exc_tb = request.exc_info
519 533 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
520 534
521 535 log.error(
522 536 'error occurred handling this request for path: %s, \n tb: %s',
523 537 request.path, traceback_info)
524 538 raise exception
525 539
526 540
527 541 class ResponseFilter(object):
528 542
529 543 def __init__(self, start_response):
530 544 self._start_response = start_response
531 545
532 546 def __call__(self, status, response_headers, exc_info=None):
533 547 headers = tuple(
534 548 (h, v) for h, v in response_headers
535 549 if not wsgiref.util.is_hop_by_hop(h))
536 550 return self._start_response(status, headers, exc_info)
537 551
538 552
539 553 def main(global_config, **settings):
540 554 if MercurialFactory:
541 555 hgpatches.patch_largefiles_capabilities()
542 556 hgpatches.patch_subrepo_type_mapping()
543 557
544 558 app = HTTPApplication(settings=settings, global_config=global_config)
545 559 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now