hooks_daemon.py
244 lines
| 7.4 KiB
| text/x-python
|
PythonLexer
r1 | # -*- coding: utf-8 -*- | |||
r1271 | # Copyright (C) 2010-2017 RhodeCode GmbH | |||
r1 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# 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 Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
import json | ||||
import logging | ||||
r1458 | import traceback | |||
r1 | import threading | |||
from BaseHTTPServer import BaseHTTPRequestHandler | ||||
from SocketServer import TCPServer | ||||
r411 | import rhodecode | |||
r669 | from rhodecode.model import meta | |||
r2335 | from rhodecode.lib.base import bootstrap_request, bootstrap_config | |||
r1 | from rhodecode.lib import hooks_base | |||
r2016 | from rhodecode.lib.utils2 import AttributeDict | |||
r1 | ||||
log = logging.getLogger(__name__) | ||||
class HooksHttpHandler(BaseHTTPRequestHandler): | ||||
def do_POST(self): | ||||
method, extras = self._read_request() | ||||
try: | ||||
result = self._call_hook(method, extras) | ||||
except Exception as e: | ||||
r1458 | exc_tb = traceback.format_exc() | |||
r1 | result = { | |||
'exception': e.__class__.__name__, | ||||
r1458 | 'exception_traceback': exc_tb, | |||
r1 | 'exception_args': e.args | |||
} | ||||
self._write_response(result) | ||||
def _read_request(self): | ||||
length = int(self.headers['Content-Length']) | ||||
body = self.rfile.read(length).decode('utf-8') | ||||
data = json.loads(body) | ||||
return data['method'], data['extras'] | ||||
def _write_response(self, result): | ||||
self.send_response(200) | ||||
self.send_header("Content-type", "text/json") | ||||
self.end_headers() | ||||
self.wfile.write(json.dumps(result)) | ||||
def _call_hook(self, method, extras): | ||||
hooks = Hooks() | ||||
r669 | try: | |||
result = getattr(hooks, method)(extras) | ||||
finally: | ||||
meta.Session.remove() | ||||
r1 | return result | |||
def log_message(self, format, *args): | ||||
""" | ||||
r1409 | This is an overridden method of BaseHTTPRequestHandler which logs using | |||
r1 | logging library instead of writing directly to stderr. | |||
""" | ||||
message = format % args | ||||
# TODO: mikhail: add different log levels support | ||||
log.debug( | ||||
"%s - - [%s] %s", self.client_address[0], | ||||
self.log_date_time_string(), message) | ||||
class DummyHooksCallbackDaemon(object): | ||||
def __init__(self): | ||||
self.hooks_module = Hooks.__module__ | ||||
def __enter__(self): | ||||
log.debug('Running dummy hooks callback daemon') | ||||
return self | ||||
def __exit__(self, exc_type, exc_val, exc_tb): | ||||
log.debug('Exiting dummy hooks callback daemon') | ||||
class ThreadedHookCallbackDaemon(object): | ||||
_callback_thread = None | ||||
_daemon = None | ||||
_done = False | ||||
def __init__(self): | ||||
self._prepare() | ||||
def __enter__(self): | ||||
self._run() | ||||
return self | ||||
def __exit__(self, exc_type, exc_val, exc_tb): | ||||
r2135 | log.debug('Callback daemon exiting now...') | |||
r1 | self._stop() | |||
def _prepare(self): | ||||
raise NotImplementedError() | ||||
def _run(self): | ||||
raise NotImplementedError() | ||||
def _stop(self): | ||||
raise NotImplementedError() | ||||
class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon): | ||||
""" | ||||
Context manager which will run a callback daemon in a background thread. | ||||
""" | ||||
hooks_uri = None | ||||
IP_ADDRESS = '127.0.0.1' | ||||
# From Python docs: Polling reduces our responsiveness to a shutdown | ||||
# request and wastes cpu at all other times. | ||||
POLL_INTERVAL = 0.1 | ||||
def _prepare(self): | ||||
r2135 | log.debug("Preparing HTTP callback daemon and registering hook object") | |||
r1 | ||||
self._done = False | ||||
self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler) | ||||
_, port = self._daemon.server_address | ||||
self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port) | ||||
log.debug("Hooks uri is: %s", self.hooks_uri) | ||||
def _run(self): | ||||
log.debug("Running event loop of callback daemon in background thread") | ||||
callback_thread = threading.Thread( | ||||
target=self._daemon.serve_forever, | ||||
kwargs={'poll_interval': self.POLL_INTERVAL}) | ||||
callback_thread.daemon = True | ||||
callback_thread.start() | ||||
self._callback_thread = callback_thread | ||||
def _stop(self): | ||||
log.debug("Waiting for background thread to finish.") | ||||
self._daemon.shutdown() | ||||
self._callback_thread.join() | ||||
self._daemon = None | ||||
self._callback_thread = None | ||||
r2408 | log.debug("Background thread done.") | |||
r1 | ||||
Martin Bornhold
|
r961 | def prepare_callback_daemon(extras, protocol, use_direct_calls): | ||
r1 | callback_daemon = None | |||
if use_direct_calls: | ||||
callback_daemon = DummyHooksCallbackDaemon() | ||||
extras['hooks_module'] = callback_daemon.hooks_module | ||||
else: | ||||
r1409 | if protocol == 'http': | |||
Martin Bornhold
|
r589 | callback_daemon = HttpHooksCallbackDaemon() | ||
else: | ||||
log.error('Unsupported callback daemon protocol "%s"', protocol) | ||||
raise Exception('Unsupported callback daemon protocol.') | ||||
r1 | extras['hooks_uri'] = callback_daemon.hooks_uri | |||
extras['hooks_protocol'] = protocol | ||||
r2135 | log.debug('Prepared a callback daemon: %s', callback_daemon) | |||
r1 | return callback_daemon, extras | |||
class Hooks(object): | ||||
""" | ||||
Exposes the hooks for remote call backs | ||||
""" | ||||
def repo_size(self, extras): | ||||
r2016 | log.debug("Called repo_size of %s object", self) | |||
r1 | return self._call_hook(hooks_base.repo_size, extras) | |||
def pre_pull(self, extras): | ||||
r2016 | log.debug("Called pre_pull of %s object", self) | |||
r1 | return self._call_hook(hooks_base.pre_pull, extras) | |||
def post_pull(self, extras): | ||||
r2016 | log.debug("Called post_pull of %s object", self) | |||
r1 | return self._call_hook(hooks_base.post_pull, extras) | |||
def pre_push(self, extras): | ||||
r2016 | log.debug("Called pre_push of %s object", self) | |||
r1 | return self._call_hook(hooks_base.pre_push, extras) | |||
def post_push(self, extras): | ||||
r2016 | log.debug("Called post_push of %s object", self) | |||
r1 | return self._call_hook(hooks_base.post_push, extras) | |||
def _call_hook(self, hook, extras): | ||||
extras = AttributeDict(extras) | ||||
r2187 | server_url = extras['server_url'] | |||
r2335 | request = bootstrap_request(application_url=server_url) | |||
bootstrap_config(request) # inject routes and other interfaces | ||||
extras.request = request | ||||
r1 | ||||
try: | ||||
result = hook(extras) | ||||
except Exception as error: | ||||
r1458 | exc_tb = traceback.format_exc() | |||
r1 | log.exception('Exception when handling hook %s', hook) | |||
error_args = error.args | ||||
return { | ||||
'status': 128, | ||||
'output': '', | ||||
'exception': type(error).__name__, | ||||
r1458 | 'exception_traceback': exc_tb, | |||
r1 | 'exception_args': error_args, | |||
} | ||||
r411 | finally: | |||
r670 | meta.Session.remove() | |||
r411 | ||||
r2187 | log.debug('Got hook call response %s', result) | |||
r1 | return { | |||
'status': result.status, | ||||
'output': result.output, | ||||
} | ||||
def __enter__(self): | ||||
return self | ||||
def __exit__(self, exc_type, exc_val, exc_tb): | ||||
pass | ||||