##// END OF EJS Templates
exception-handling: propagate hooks tracebacks to vcsserver for easier debugging.
marcink -
r1458:cf13f14c default
parent child Browse files
Show More
@@ -1,238 +1,241 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 import urlparse
23 import traceback
24 24 import threading
25 25 from BaseHTTPServer import BaseHTTPRequestHandler
26 26 from SocketServer import TCPServer
27 from routes.util import URLGenerator
28 27
29 28 import pylons
30 29 import rhodecode
31 30
32 31 from rhodecode.model import meta
33 32 from rhodecode.lib import hooks_base
34 33 from rhodecode.lib.utils2 import (
35 34 AttributeDict, safe_str, get_routes_generator_for_server_url)
36 35
37 36
38 37 log = logging.getLogger(__name__)
39 38
40 39
41 40 class HooksHttpHandler(BaseHTTPRequestHandler):
42 41 def do_POST(self):
43 42 method, extras = self._read_request()
44 43 try:
45 44 result = self._call_hook(method, extras)
46 45 except Exception as e:
46 exc_tb = traceback.format_exc()
47 47 result = {
48 48 'exception': e.__class__.__name__,
49 'exception_traceback': exc_tb,
49 50 'exception_args': e.args
50 51 }
51 52 self._write_response(result)
52 53
53 54 def _read_request(self):
54 55 length = int(self.headers['Content-Length'])
55 56 body = self.rfile.read(length).decode('utf-8')
56 57 data = json.loads(body)
57 58 return data['method'], data['extras']
58 59
59 60 def _write_response(self, result):
60 61 self.send_response(200)
61 62 self.send_header("Content-type", "text/json")
62 63 self.end_headers()
63 64 self.wfile.write(json.dumps(result))
64 65
65 66 def _call_hook(self, method, extras):
66 67 hooks = Hooks()
67 68 try:
68 69 result = getattr(hooks, method)(extras)
69 70 finally:
70 71 meta.Session.remove()
71 72 return result
72 73
73 74 def log_message(self, format, *args):
74 75 """
75 76 This is an overridden method of BaseHTTPRequestHandler which logs using
76 77 logging library instead of writing directly to stderr.
77 78 """
78 79
79 80 message = format % args
80 81
81 82 # TODO: mikhail: add different log levels support
82 83 log.debug(
83 84 "%s - - [%s] %s", self.client_address[0],
84 85 self.log_date_time_string(), message)
85 86
86 87
87 88 class DummyHooksCallbackDaemon(object):
88 89 def __init__(self):
89 90 self.hooks_module = Hooks.__module__
90 91
91 92 def __enter__(self):
92 93 log.debug('Running dummy hooks callback daemon')
93 94 return self
94 95
95 96 def __exit__(self, exc_type, exc_val, exc_tb):
96 97 log.debug('Exiting dummy hooks callback daemon')
97 98
98 99
99 100 class ThreadedHookCallbackDaemon(object):
100 101
101 102 _callback_thread = None
102 103 _daemon = None
103 104 _done = False
104 105
105 106 def __init__(self):
106 107 self._prepare()
107 108
108 109 def __enter__(self):
109 110 self._run()
110 111 return self
111 112
112 113 def __exit__(self, exc_type, exc_val, exc_tb):
113 114 self._stop()
114 115
115 116 def _prepare(self):
116 117 raise NotImplementedError()
117 118
118 119 def _run(self):
119 120 raise NotImplementedError()
120 121
121 122 def _stop(self):
122 123 raise NotImplementedError()
123 124
124 125
125 126 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
126 127 """
127 128 Context manager which will run a callback daemon in a background thread.
128 129 """
129 130
130 131 hooks_uri = None
131 132
132 133 IP_ADDRESS = '127.0.0.1'
133 134
134 135 # From Python docs: Polling reduces our responsiveness to a shutdown
135 136 # request and wastes cpu at all other times.
136 137 POLL_INTERVAL = 0.1
137 138
138 139 def _prepare(self):
139 140 log.debug("Preparing callback daemon and registering hook object")
140 141
141 142 self._done = False
142 143 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
143 144 _, port = self._daemon.server_address
144 145 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
145 146
146 147 log.debug("Hooks uri is: %s", self.hooks_uri)
147 148
148 149 def _run(self):
149 150 log.debug("Running event loop of callback daemon in background thread")
150 151 callback_thread = threading.Thread(
151 152 target=self._daemon.serve_forever,
152 153 kwargs={'poll_interval': self.POLL_INTERVAL})
153 154 callback_thread.daemon = True
154 155 callback_thread.start()
155 156 self._callback_thread = callback_thread
156 157
157 158 def _stop(self):
158 159 log.debug("Waiting for background thread to finish.")
159 160 self._daemon.shutdown()
160 161 self._callback_thread.join()
161 162 self._daemon = None
162 163 self._callback_thread = None
163 164
164 165
165 166 def prepare_callback_daemon(extras, protocol, use_direct_calls):
166 167 callback_daemon = None
167 168
168 169 if use_direct_calls:
169 170 callback_daemon = DummyHooksCallbackDaemon()
170 171 extras['hooks_module'] = callback_daemon.hooks_module
171 172 else:
172 173 if protocol == 'http':
173 174 callback_daemon = HttpHooksCallbackDaemon()
174 175 else:
175 176 log.error('Unsupported callback daemon protocol "%s"', protocol)
176 177 raise Exception('Unsupported callback daemon protocol.')
177 178
178 179 extras['hooks_uri'] = callback_daemon.hooks_uri
179 180 extras['hooks_protocol'] = protocol
180 181
181 182 return callback_daemon, extras
182 183
183 184
184 185 class Hooks(object):
185 186 """
186 187 Exposes the hooks for remote call backs
187 188 """
188 189
189 190 def repo_size(self, extras):
190 191 log.debug("Called repo_size of Hooks object")
191 192 return self._call_hook(hooks_base.repo_size, extras)
192 193
193 194 def pre_pull(self, extras):
194 195 log.debug("Called pre_pull of Hooks object")
195 196 return self._call_hook(hooks_base.pre_pull, extras)
196 197
197 198 def post_pull(self, extras):
198 199 log.debug("Called post_pull of Hooks object")
199 200 return self._call_hook(hooks_base.post_pull, extras)
200 201
201 202 def pre_push(self, extras):
202 203 log.debug("Called pre_push of Hooks object")
203 204 return self._call_hook(hooks_base.pre_push, extras)
204 205
205 206 def post_push(self, extras):
206 207 log.debug("Called post_push of Hooks object")
207 208 return self._call_hook(hooks_base.post_push, extras)
208 209
209 210 def _call_hook(self, hook, extras):
210 211 extras = AttributeDict(extras)
211 212 pylons_router = get_routes_generator_for_server_url(extras.server_url)
212 213 pylons.url._push_object(pylons_router)
213 214
214 215 try:
215 216 result = hook(extras)
216 217 except Exception as error:
218 exc_tb = traceback.format_exc()
217 219 log.exception('Exception when handling hook %s', hook)
218 220 error_args = error.args
219 221 return {
220 222 'status': 128,
221 223 'output': '',
222 224 'exception': type(error).__name__,
225 'exception_traceback': exc_tb,
223 226 'exception_args': error_args,
224 227 }
225 228 finally:
226 229 pylons.url._pop_object()
227 230 meta.Session.remove()
228 231
229 232 return {
230 233 'status': result.status,
231 234 'output': result.output,
232 235 }
233 236
234 237 def __enter__(self):
235 238 return self
236 239
237 240 def __exit__(self, exc_type, exc_val, exc_tb):
238 241 pass
General Comments 0
You need to be logged in to leave comments. Login now