##// END OF EJS Templates
release: merge back stable branch into default
release: merge back stable branch into default

File last commit:

r0:0fb8cb8f default
r63:265fb3a0 merge default
Show More
main.py
507 lines | 16.5 KiB | text/x-python | PythonLexer
# RhodeCode VCSServer provides access to different vcs backends via network.
# Copyright (C) 2014-2016 RodeCode GmbH
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import atexit
import locale
import logging
import optparse
import os
import textwrap
import threading
import sys
import configobj
import Pyro4
from beaker.cache import CacheManager
from beaker.util import parse_cache_config_options
try:
from vcsserver.git import GitFactory, GitRemote
except ImportError:
GitFactory = None
GitRemote = None
try:
from vcsserver.hg import MercurialFactory, HgRemote
except ImportError:
MercurialFactory = None
HgRemote = None
try:
from vcsserver.svn import SubversionFactory, SvnRemote
except ImportError:
SubversionFactory = None
SvnRemote = None
from server import VcsServer
from vcsserver import hgpatches, remote_wsgi, settings
from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
log = logging.getLogger(__name__)
HERE = os.path.dirname(os.path.abspath(__file__))
SERVER_RUNNING_FILE = None
# HOOKS - inspired by gunicorn #
def when_ready(server):
"""
Called just after the server is started.
"""
def _remove_server_running_file():
if os.path.isfile(SERVER_RUNNING_FILE):
os.remove(SERVER_RUNNING_FILE)
# top up to match to level location
if SERVER_RUNNING_FILE:
with open(SERVER_RUNNING_FILE, 'wb') as f:
f.write(str(os.getpid()))
# register cleanup of that file when server exits
atexit.register(_remove_server_running_file)
class LazyWriter(object):
"""
File-like object that opens a file lazily when it is first written
to.
"""
def __init__(self, filename, mode='w'):
self.filename = filename
self.fileobj = None
self.lock = threading.Lock()
self.mode = mode
def open(self):
if self.fileobj is None:
with self.lock:
self.fileobj = open(self.filename, self.mode)
return self.fileobj
def close(self):
fileobj = self.fileobj
if fileobj is not None:
fileobj.close()
def __del__(self):
self.close()
def write(self, text):
fileobj = self.open()
fileobj.write(text)
fileobj.flush()
def writelines(self, text):
fileobj = self.open()
fileobj.writelines(text)
fileobj.flush()
def flush(self):
self.open().flush()
class Application(object):
"""
Represents the vcs server application.
This object is responsible to initialize the application and all needed
libraries. After that it hooks together the different objects and provides
them a way to access things like configuration.
"""
def __init__(
self, host, port=None, locale='', threadpool_size=None,
timeout=None, cache_config=None, remote_wsgi_=None):
self.host = host
self.port = int(port) or settings.PYRO_PORT
self.threadpool_size = (
int(threadpool_size) if threadpool_size else None)
self.locale = locale
self.timeout = timeout
self.cache_config = cache_config
self.remote_wsgi = remote_wsgi_ or remote_wsgi
def init(self):
"""
Configure and hook together all relevant objects.
"""
self._configure_locale()
self._configure_pyro()
self._initialize_cache()
self._create_daemon_and_remote_objects(host=self.host, port=self.port)
def run(self):
"""
Start the main loop of the application.
"""
if hasattr(os, 'getpid'):
log.info('Starting %s in PID %i.', __name__, os.getpid())
else:
log.info('Starting %s.', __name__)
if SERVER_RUNNING_FILE:
log.info('PID file written as %s', SERVER_RUNNING_FILE)
else:
log.info('No PID file written by default.')
when_ready(self)
try:
self._pyrodaemon.requestLoop(
loopCondition=lambda: not self._vcsserver._shutdown)
finally:
self._pyrodaemon.shutdown()
def _configure_locale(self):
if self.locale:
log.info('Settings locale: `LC_ALL` to %s' % self.locale)
else:
log.info(
'Configuring locale subsystem based on environment variables')
try:
# If self.locale is the empty string, then the locale
# module will use the environment variables. See the
# documentation of the package `locale`.
locale.setlocale(locale.LC_ALL, self.locale)
language_code, encoding = locale.getlocale()
log.info(
'Locale set to language code "%s" with encoding "%s".',
language_code, encoding)
except locale.Error:
log.exception(
'Cannot set locale, not configuring the locale system')
def _configure_pyro(self):
if self.threadpool_size is not None:
log.info("Threadpool size set to %s", self.threadpool_size)
Pyro4.config.THREADPOOL_SIZE = self.threadpool_size
if self.timeout not in (None, 0, 0.0, '0'):
log.info("Timeout for RPC calls set to %s seconds", self.timeout)
Pyro4.config.COMMTIMEOUT = float(self.timeout)
Pyro4.config.SERIALIZER = 'pickle'
Pyro4.config.SERIALIZERS_ACCEPTED.add('pickle')
Pyro4.config.SOCK_REUSE = True
# Uncomment the next line when you need to debug remote errors
# Pyro4.config.DETAILED_TRACEBACK = True
def _initialize_cache(self):
cache_config = parse_cache_config_options(self.cache_config)
log.info('Initializing beaker cache: %s' % cache_config)
self.cache = CacheManager(**cache_config)
def _create_daemon_and_remote_objects(self, host='localhost',
port=settings.PYRO_PORT):
daemon = Pyro4.Daemon(host=host, port=port)
self._vcsserver = VcsServer()
uri = daemon.register(
self._vcsserver, objectId=settings.PYRO_VCSSERVER)
log.info("Object registered = %s", uri)
if GitFactory and GitRemote:
git_repo_cache = self.cache.get_cache_region('git', region='repo_object')
git_factory = GitFactory(git_repo_cache)
self._git_remote = GitRemote(git_factory)
uri = daemon.register(self._git_remote, objectId=settings.PYRO_GIT)
log.info("Object registered = %s", uri)
else:
log.info("Git client import failed")
if MercurialFactory and HgRemote:
hg_repo_cache = self.cache.get_cache_region('hg', region='repo_object')
hg_factory = MercurialFactory(hg_repo_cache)
self._hg_remote = HgRemote(hg_factory)
uri = daemon.register(self._hg_remote, objectId=settings.PYRO_HG)
log.info("Object registered = %s", uri)
else:
log.info("Mercurial client import failed")
if SubversionFactory and SvnRemote:
svn_repo_cache = self.cache.get_cache_region('svn', region='repo_object')
svn_factory = SubversionFactory(svn_repo_cache)
self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
uri = daemon.register(self._svn_remote, objectId=settings.PYRO_SVN)
log.info("Object registered = %s", uri)
else:
log.info("Subversion client import failed")
self._git_remote_wsgi = self.remote_wsgi.GitRemoteWsgi()
uri = daemon.register(self._git_remote_wsgi,
objectId=settings.PYRO_GIT_REMOTE_WSGI)
log.info("Object registered = %s", uri)
self._hg_remote_wsgi = self.remote_wsgi.HgRemoteWsgi()
uri = daemon.register(self._hg_remote_wsgi,
objectId=settings.PYRO_HG_REMOTE_WSGI)
log.info("Object registered = %s", uri)
self._pyrodaemon = daemon
class VcsServerCommand(object):
usage = '%prog'
description = """
Runs the VCS server
"""
default_verbosity = 1
parser = optparse.OptionParser(
usage,
description=textwrap.dedent(description)
)
parser.add_option(
'--host',
type="str",
dest="host",
)
parser.add_option(
'--port',
type="int",
dest="port"
)
parser.add_option(
'--running-file',
dest='running_file',
metavar='RUNNING_FILE',
help="Create a running file after the server is initalized with "
"stored PID of process"
)
parser.add_option(
'--locale',
dest='locale',
help="Allows to set the locale, e.g. en_US.UTF-8",
default=""
)
parser.add_option(
'--log-file',
dest='log_file',
metavar='LOG_FILE',
help="Save output to the given log file (redirects stdout)"
)
parser.add_option(
'--log-level',
dest="log_level",
metavar="LOG_LEVEL",
help="use LOG_LEVEL to set log level "
"(debug,info,warning,error,critical)"
)
parser.add_option(
'--threadpool',
dest='threadpool_size',
type='int',
help="Set the size of the threadpool used to communicate with the "
"WSGI workers. This should be at least 6 times the number of "
"WSGI worker processes."
)
parser.add_option(
'--timeout',
dest='timeout',
type='float',
help="Set the timeout for RPC communication in seconds."
)
parser.add_option(
'--config',
dest='config_file',
type='string',
help="Configuration file for vcsserver."
)
def __init__(self, argv, quiet=False):
self.options, self.args = self.parser.parse_args(argv[1:])
if quiet:
self.options.verbose = 0
def _get_file_config(self):
ini_conf = {}
conf = configobj.ConfigObj(self.options.config_file)
if 'DEFAULT' in conf:
ini_conf = conf['DEFAULT']
return ini_conf
def _show_config(self, vcsserver_config):
order = [
'config_file',
'host',
'port',
'log_file',
'log_level',
'locale',
'threadpool_size',
'timeout',
'cache_config',
]
def sorter(k):
return dict([(y, x) for x, y in enumerate(order)]).get(k)
_config = []
for k in sorted(vcsserver_config.keys(), key=sorter):
v = vcsserver_config[k]
# construct padded key for display eg %-20s % = key: val
k_formatted = ('%-'+str(len(max(order, key=len))+1)+'s') % (k+':')
_config.append(' * %s %s' % (k_formatted, v))
log.info('\n[vcsserver configuration]:\n'+'\n'.join(_config))
def _get_vcsserver_configuration(self):
_defaults = {
'config_file': None,
'git_path': 'git',
'host': 'localhost',
'port': settings.PYRO_PORT,
'log_file': None,
'log_level': 'debug',
'locale': None,
'threadpool_size': 16,
'timeout': None,
# Development support
'dev.use_echo_app': False,
# caches, baker style config
'beaker.cache.regions': 'repo_object',
'beaker.cache.repo_object.expire': '10',
'beaker.cache.repo_object.type': 'memory',
}
config = {}
config.update(_defaults)
# overwrite defaults with one loaded from file
config.update(self._get_file_config())
# overwrite with self.option which has the top priority
for k, v in self.options.__dict__.items():
if v or v == 0:
config[k] = v
# clear all "extra" keys if they are somehow passed,
# we only want defaults, so any extra stuff from self.options is cleared
# except beaker stuff which needs to be dynamic
for k in [k for k in config.copy().keys() if not k.startswith('beaker.cache.')]:
if k not in _defaults:
del config[k]
# group together the cache into one key.
# Needed further for beaker lib configuration
_k = {}
for k in [k for k in config.copy() if k.startswith('beaker.cache.')]:
_k[k] = config.pop(k)
config['cache_config'] = _k
return config
def out(self, msg): # pragma: no cover
if self.options.verbose > 0:
print(msg)
def run(self): # pragma: no cover
vcsserver_config = self._get_vcsserver_configuration()
# Ensure the log file is writeable
if vcsserver_config['log_file']:
stdout_log = self._configure_logfile()
else:
stdout_log = None
# set PID file with running lock
if self.options.running_file:
global SERVER_RUNNING_FILE
SERVER_RUNNING_FILE = self.options.running_file
# configure logging, and logging based on configuration file
self._configure_logging(level=vcsserver_config['log_level'],
stream=stdout_log)
if self.options.config_file:
if not os.path.isfile(self.options.config_file):
raise OSError('File %s does not exist' %
self.options.config_file)
self._configure_file_logging(self.options.config_file)
self._configure_settings(vcsserver_config)
# display current configuration of vcsserver
self._show_config(vcsserver_config)
if not vcsserver_config['dev.use_echo_app']:
remote_wsgi_mod = remote_wsgi
else:
log.warning("Using EchoApp for VCS endpoints.")
remote_wsgi_mod = remote_wsgi_stub
app = Application(
host=vcsserver_config['host'],
port=vcsserver_config['port'],
locale=vcsserver_config['locale'],
threadpool_size=vcsserver_config['threadpool_size'],
timeout=vcsserver_config['timeout'],
cache_config=vcsserver_config['cache_config'],
remote_wsgi_=remote_wsgi_mod)
app.init()
app.run()
def _configure_logging(self, level, stream=None):
_format = (
'%(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s')
levels = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL,
}
try:
level = levels[level]
except KeyError:
raise AttributeError(
'Invalid log level please use one of %s' % (levels.keys(),))
logging.basicConfig(format=_format, stream=stream, level=level)
logging.getLogger('Pyro4').setLevel(level)
def _configure_file_logging(self, config):
import logging.config
try:
logging.config.fileConfig(config)
except Exception as e:
log.warning('Failed to configure logging based on given '
'config file. Error: %s' % e)
def _configure_logfile(self):
try:
writeable_log_file = open(self.options.log_file, 'a')
except IOError as ioe:
msg = 'Error: Unable to write to log file: %s' % ioe
raise ValueError(msg)
writeable_log_file.close()
stdout_log = LazyWriter(self.options.log_file, 'a')
sys.stdout = stdout_log
sys.stderr = stdout_log
return stdout_log
def _configure_settings(self, config):
"""
Configure the settings module based on the given `config`.
"""
settings.GIT_EXECUTABLE = config['git_path']
def main(argv=sys.argv, quiet=False):
if MercurialFactory:
hgpatches.patch_largefiles_capabilities()
command = VcsServerCommand(argv, quiet=quiet)
return command.run()