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