##// END OF EJS Templates
vcs: Raise an error in case of unsupported protocol.
Martin Bornhold -
r589:edece7f1 default
parent child Browse files
Show More
@@ -1,270 +1,274 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22 import logging
22 import logging
23 import urlparse
23 import urlparse
24 import threading
24 import threading
25 from BaseHTTPServer import BaseHTTPRequestHandler
25 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
26 from SocketServer import TCPServer
27 from routes.util import URLGenerator
27 from routes.util import URLGenerator
28
28
29 import Pyro4
29 import Pyro4
30 import pylons
30 import pylons
31 import rhodecode
31 import rhodecode
32
32
33 from rhodecode.lib import hooks_base
33 from rhodecode.lib import hooks_base
34 from rhodecode.lib.utils2 import (
34 from rhodecode.lib.utils2 import (
35 AttributeDict, safe_str, get_routes_generator_for_server_url)
35 AttributeDict, safe_str, get_routes_generator_for_server_url)
36
36
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class HooksHttpHandler(BaseHTTPRequestHandler):
41 class HooksHttpHandler(BaseHTTPRequestHandler):
42 def do_POST(self):
42 def do_POST(self):
43 method, extras = self._read_request()
43 method, extras = self._read_request()
44 try:
44 try:
45 result = self._call_hook(method, extras)
45 result = self._call_hook(method, extras)
46 except Exception as e:
46 except Exception as e:
47 result = {
47 result = {
48 'exception': e.__class__.__name__,
48 'exception': e.__class__.__name__,
49 'exception_args': e.args
49 'exception_args': e.args
50 }
50 }
51 self._write_response(result)
51 self._write_response(result)
52
52
53 def _read_request(self):
53 def _read_request(self):
54 length = int(self.headers['Content-Length'])
54 length = int(self.headers['Content-Length'])
55 body = self.rfile.read(length).decode('utf-8')
55 body = self.rfile.read(length).decode('utf-8')
56 data = json.loads(body)
56 data = json.loads(body)
57 return data['method'], data['extras']
57 return data['method'], data['extras']
58
58
59 def _write_response(self, result):
59 def _write_response(self, result):
60 self.send_response(200)
60 self.send_response(200)
61 self.send_header("Content-type", "text/json")
61 self.send_header("Content-type", "text/json")
62 self.end_headers()
62 self.end_headers()
63 self.wfile.write(json.dumps(result))
63 self.wfile.write(json.dumps(result))
64
64
65 def _call_hook(self, method, extras):
65 def _call_hook(self, method, extras):
66 hooks = Hooks()
66 hooks = Hooks()
67 result = getattr(hooks, method)(extras)
67 result = getattr(hooks, method)(extras)
68 return result
68 return result
69
69
70 def log_message(self, format, *args):
70 def log_message(self, format, *args):
71 """
71 """
72 This is an overriden method of BaseHTTPRequestHandler which logs using
72 This is an overriden method of BaseHTTPRequestHandler which logs using
73 logging library instead of writing directly to stderr.
73 logging library instead of writing directly to stderr.
74 """
74 """
75
75
76 message = format % args
76 message = format % args
77
77
78 # TODO: mikhail: add different log levels support
78 # TODO: mikhail: add different log levels support
79 log.debug(
79 log.debug(
80 "%s - - [%s] %s", self.client_address[0],
80 "%s - - [%s] %s", self.client_address[0],
81 self.log_date_time_string(), message)
81 self.log_date_time_string(), message)
82
82
83
83
84 class DummyHooksCallbackDaemon(object):
84 class DummyHooksCallbackDaemon(object):
85 def __init__(self):
85 def __init__(self):
86 self.hooks_module = Hooks.__module__
86 self.hooks_module = Hooks.__module__
87
87
88 def __enter__(self):
88 def __enter__(self):
89 log.debug('Running dummy hooks callback daemon')
89 log.debug('Running dummy hooks callback daemon')
90 return self
90 return self
91
91
92 def __exit__(self, exc_type, exc_val, exc_tb):
92 def __exit__(self, exc_type, exc_val, exc_tb):
93 log.debug('Exiting dummy hooks callback daemon')
93 log.debug('Exiting dummy hooks callback daemon')
94
94
95
95
96 class ThreadedHookCallbackDaemon(object):
96 class ThreadedHookCallbackDaemon(object):
97
97
98 _callback_thread = None
98 _callback_thread = None
99 _daemon = None
99 _daemon = None
100 _done = False
100 _done = False
101
101
102 def __init__(self):
102 def __init__(self):
103 self._prepare()
103 self._prepare()
104
104
105 def __enter__(self):
105 def __enter__(self):
106 self._run()
106 self._run()
107 return self
107 return self
108
108
109 def __exit__(self, exc_type, exc_val, exc_tb):
109 def __exit__(self, exc_type, exc_val, exc_tb):
110 self._stop()
110 self._stop()
111
111
112 def _prepare(self):
112 def _prepare(self):
113 raise NotImplementedError()
113 raise NotImplementedError()
114
114
115 def _run(self):
115 def _run(self):
116 raise NotImplementedError()
116 raise NotImplementedError()
117
117
118 def _stop(self):
118 def _stop(self):
119 raise NotImplementedError()
119 raise NotImplementedError()
120
120
121
121
122 class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
122 class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
123 """
123 """
124 Context manager which will run a callback daemon in a background thread.
124 Context manager which will run a callback daemon in a background thread.
125 """
125 """
126
126
127 hooks_uri = None
127 hooks_uri = None
128
128
129 def _prepare(self):
129 def _prepare(self):
130 log.debug("Preparing callback daemon and registering hook object")
130 log.debug("Preparing callback daemon and registering hook object")
131 self._daemon = Pyro4.Daemon()
131 self._daemon = Pyro4.Daemon()
132 hooks_interface = Hooks()
132 hooks_interface = Hooks()
133 self.hooks_uri = str(self._daemon.register(hooks_interface))
133 self.hooks_uri = str(self._daemon.register(hooks_interface))
134 log.debug("Hooks uri is: %s", self.hooks_uri)
134 log.debug("Hooks uri is: %s", self.hooks_uri)
135
135
136 def _run(self):
136 def _run(self):
137 log.debug("Running event loop of callback daemon in background thread")
137 log.debug("Running event loop of callback daemon in background thread")
138 callback_thread = threading.Thread(
138 callback_thread = threading.Thread(
139 target=self._daemon.requestLoop,
139 target=self._daemon.requestLoop,
140 kwargs={'loopCondition': lambda: not self._done})
140 kwargs={'loopCondition': lambda: not self._done})
141 callback_thread.daemon = True
141 callback_thread.daemon = True
142 callback_thread.start()
142 callback_thread.start()
143 self._callback_thread = callback_thread
143 self._callback_thread = callback_thread
144
144
145 def _stop(self):
145 def _stop(self):
146 log.debug("Waiting for background thread to finish.")
146 log.debug("Waiting for background thread to finish.")
147 self._done = True
147 self._done = True
148 self._callback_thread.join()
148 self._callback_thread.join()
149 self._daemon.close()
149 self._daemon.close()
150 self._daemon = None
150 self._daemon = None
151 self._callback_thread = None
151 self._callback_thread = None
152
152
153
153
154 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
154 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
155 """
155 """
156 Context manager which will run a callback daemon in a background thread.
156 Context manager which will run a callback daemon in a background thread.
157 """
157 """
158
158
159 hooks_uri = None
159 hooks_uri = None
160
160
161 IP_ADDRESS = '127.0.0.1'
161 IP_ADDRESS = '127.0.0.1'
162
162
163 # From Python docs: Polling reduces our responsiveness to a shutdown
163 # From Python docs: Polling reduces our responsiveness to a shutdown
164 # request and wastes cpu at all other times.
164 # request and wastes cpu at all other times.
165 POLL_INTERVAL = 0.1
165 POLL_INTERVAL = 0.1
166
166
167 def _prepare(self):
167 def _prepare(self):
168 log.debug("Preparing callback daemon and registering hook object")
168 log.debug("Preparing callback daemon and registering hook object")
169
169
170 self._done = False
170 self._done = False
171 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
171 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
172 _, port = self._daemon.server_address
172 _, port = self._daemon.server_address
173 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
173 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
174
174
175 log.debug("Hooks uri is: %s", self.hooks_uri)
175 log.debug("Hooks uri is: %s", self.hooks_uri)
176
176
177 def _run(self):
177 def _run(self):
178 log.debug("Running event loop of callback daemon in background thread")
178 log.debug("Running event loop of callback daemon in background thread")
179 callback_thread = threading.Thread(
179 callback_thread = threading.Thread(
180 target=self._daemon.serve_forever,
180 target=self._daemon.serve_forever,
181 kwargs={'poll_interval': self.POLL_INTERVAL})
181 kwargs={'poll_interval': self.POLL_INTERVAL})
182 callback_thread.daemon = True
182 callback_thread.daemon = True
183 callback_thread.start()
183 callback_thread.start()
184 self._callback_thread = callback_thread
184 self._callback_thread = callback_thread
185
185
186 def _stop(self):
186 def _stop(self):
187 log.debug("Waiting for background thread to finish.")
187 log.debug("Waiting for background thread to finish.")
188 self._daemon.shutdown()
188 self._daemon.shutdown()
189 self._callback_thread.join()
189 self._callback_thread.join()
190 self._daemon = None
190 self._daemon = None
191 self._callback_thread = None
191 self._callback_thread = None
192
192
193
193
194 def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
194 def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
195 callback_daemon = None
195 callback_daemon = None
196 protocol = protocol.lower() if protocol else None
196 protocol = protocol.lower() if protocol else None
197
197
198 if use_direct_calls:
198 if use_direct_calls:
199 callback_daemon = DummyHooksCallbackDaemon()
199 callback_daemon = DummyHooksCallbackDaemon()
200 extras['hooks_module'] = callback_daemon.hooks_module
200 extras['hooks_module'] = callback_daemon.hooks_module
201 else:
201 else:
202 callback_daemon = (
202 if protocol == 'pyro4':
203 Pyro4HooksCallbackDaemon()
203 callback_daemon = Pyro4HooksCallbackDaemon()
204 if protocol == 'pyro4'
204 elif protocol == 'http':
205 else HttpHooksCallbackDaemon())
205 callback_daemon = HttpHooksCallbackDaemon()
206 else:
207 log.error('Unsupported callback daemon protocol "%s"', protocol)
208 raise Exception('Unsupported callback daemon protocol.')
209
206 extras['hooks_uri'] = callback_daemon.hooks_uri
210 extras['hooks_uri'] = callback_daemon.hooks_uri
207 extras['hooks_protocol'] = protocol
211 extras['hooks_protocol'] = protocol
208
212
209 return callback_daemon, extras
213 return callback_daemon, extras
210
214
211
215
212 class Hooks(object):
216 class Hooks(object):
213 """
217 """
214 Exposes the hooks for remote call backs
218 Exposes the hooks for remote call backs
215 """
219 """
216
220
217 @Pyro4.callback
221 @Pyro4.callback
218 def repo_size(self, extras):
222 def repo_size(self, extras):
219 log.debug("Called repo_size of Hooks object")
223 log.debug("Called repo_size of Hooks object")
220 return self._call_hook(hooks_base.repo_size, extras)
224 return self._call_hook(hooks_base.repo_size, extras)
221
225
222 @Pyro4.callback
226 @Pyro4.callback
223 def pre_pull(self, extras):
227 def pre_pull(self, extras):
224 log.debug("Called pre_pull of Hooks object")
228 log.debug("Called pre_pull of Hooks object")
225 return self._call_hook(hooks_base.pre_pull, extras)
229 return self._call_hook(hooks_base.pre_pull, extras)
226
230
227 @Pyro4.callback
231 @Pyro4.callback
228 def post_pull(self, extras):
232 def post_pull(self, extras):
229 log.debug("Called post_pull of Hooks object")
233 log.debug("Called post_pull of Hooks object")
230 return self._call_hook(hooks_base.post_pull, extras)
234 return self._call_hook(hooks_base.post_pull, extras)
231
235
232 @Pyro4.callback
236 @Pyro4.callback
233 def pre_push(self, extras):
237 def pre_push(self, extras):
234 log.debug("Called pre_push of Hooks object")
238 log.debug("Called pre_push of Hooks object")
235 return self._call_hook(hooks_base.pre_push, extras)
239 return self._call_hook(hooks_base.pre_push, extras)
236
240
237 @Pyro4.callback
241 @Pyro4.callback
238 def post_push(self, extras):
242 def post_push(self, extras):
239 log.debug("Called post_push of Hooks object")
243 log.debug("Called post_push of Hooks object")
240 return self._call_hook(hooks_base.post_push, extras)
244 return self._call_hook(hooks_base.post_push, extras)
241
245
242 def _call_hook(self, hook, extras):
246 def _call_hook(self, hook, extras):
243 extras = AttributeDict(extras)
247 extras = AttributeDict(extras)
244 pylons_router = get_routes_generator_for_server_url(extras.server_url)
248 pylons_router = get_routes_generator_for_server_url(extras.server_url)
245 pylons.url._push_object(pylons_router)
249 pylons.url._push_object(pylons_router)
246
250
247 try:
251 try:
248 result = hook(extras)
252 result = hook(extras)
249 except Exception as error:
253 except Exception as error:
250 log.exception('Exception when handling hook %s', hook)
254 log.exception('Exception when handling hook %s', hook)
251 error_args = error.args
255 error_args = error.args
252 return {
256 return {
253 'status': 128,
257 'status': 128,
254 'output': '',
258 'output': '',
255 'exception': type(error).__name__,
259 'exception': type(error).__name__,
256 'exception_args': error_args,
260 'exception_args': error_args,
257 }
261 }
258 finally:
262 finally:
259 pylons.url._pop_object()
263 pylons.url._pop_object()
260
264
261 return {
265 return {
262 'status': result.status,
266 'status': result.status,
263 'output': result.output,
267 'output': result.output,
264 }
268 }
265
269
266 def __enter__(self):
270 def __enter__(self):
267 return self
271 return self
268
272
269 def __exit__(self, exc_type, exc_val, exc_tb):
273 def __exit__(self, exc_type, exc_val, exc_tb):
270 pass
274 pass
General Comments 0
You need to be logged in to leave comments. Login now