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