##// END OF EJS Templates
hooks: expose pushed refs inside hooks....
marcink -
r223:a7b3535b default
parent child Browse files
Show More
@@ -1,396 +1,426 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2017 RodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import io
21 21 import sys
22 22 import json
23 23 import logging
24 24 import collections
25 25 import importlib
26 26 import subprocess
27 27
28 28 from httplib import HTTPConnection
29 29
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33 import simplejson as json
34 34
35 35 from vcsserver import exceptions
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HooksHttpClient(object):
41 41 connection = None
42 42
43 43 def __init__(self, hooks_uri):
44 44 self.hooks_uri = hooks_uri
45 45
46 46 def __call__(self, method, extras):
47 47 connection = HTTPConnection(self.hooks_uri)
48 48 body = self._serialize(method, extras)
49 49 connection.request('POST', '/', body)
50 50 response = connection.getresponse()
51 51 return json.loads(response.read())
52 52
53 53 def _serialize(self, hook_name, extras):
54 54 data = {
55 55 'method': hook_name,
56 56 'extras': extras
57 57 }
58 58 return json.dumps(data)
59 59
60 60
61 61 class HooksDummyClient(object):
62 62 def __init__(self, hooks_module):
63 63 self._hooks_module = importlib.import_module(hooks_module)
64 64
65 65 def __call__(self, hook_name, extras):
66 66 with self._hooks_module.Hooks() as hooks:
67 67 return getattr(hooks, hook_name)(extras)
68 68
69 69
70 70 class RemoteMessageWriter(object):
71 71 """Writer base class."""
72 72 def write(self, message):
73 73 raise NotImplementedError()
74 74
75 75
76 76 class HgMessageWriter(RemoteMessageWriter):
77 77 """Writer that knows how to send messages to mercurial clients."""
78 78
79 79 def __init__(self, ui):
80 80 self.ui = ui
81 81
82 82 def write(self, message):
83 83 # TODO: Check why the quiet flag is set by default.
84 84 old = self.ui.quiet
85 85 self.ui.quiet = False
86 86 self.ui.status(message.encode('utf-8'))
87 87 self.ui.quiet = old
88 88
89 89
90 90 class GitMessageWriter(RemoteMessageWriter):
91 91 """Writer that knows how to send messages to git clients."""
92 92
93 93 def __init__(self, stdout=None):
94 94 self.stdout = stdout or sys.stdout
95 95
96 96 def write(self, message):
97 97 self.stdout.write(message.encode('utf-8'))
98 98
99 99
100 100 def _handle_exception(result):
101 101 exception_class = result.get('exception')
102 102 exception_traceback = result.get('exception_traceback')
103 103
104 104 if exception_traceback:
105 105 log.error('Got traceback from remote call:%s', exception_traceback)
106 106
107 107 if exception_class == 'HTTPLockedRC':
108 108 raise exceptions.RepositoryLockedException(*result['exception_args'])
109 109 elif exception_class == 'RepositoryError':
110 110 raise exceptions.VcsException(*result['exception_args'])
111 111 elif exception_class:
112 112 raise Exception('Got remote exception "%s" with args "%s"' %
113 113 (exception_class, result['exception_args']))
114 114
115 115
116 116 def _get_hooks_client(extras):
117 117 if 'hooks_uri' in extras:
118 118 protocol = extras.get('hooks_protocol')
119 119 return HooksHttpClient(extras['hooks_uri'])
120 120 else:
121 121 return HooksDummyClient(extras['hooks_module'])
122 122
123 123
124 124 def _call_hook(hook_name, extras, writer):
125 125 hooks = _get_hooks_client(extras)
126 126 result = hooks(hook_name, extras)
127 127 writer.write(result['output'])
128 128 _handle_exception(result)
129 129
130 130 return result['status']
131 131
132 132
133 133 def _extras_from_ui(ui):
134 134 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
135 135 return extras
136 136
137 137
138 138 def repo_size(ui, repo, **kwargs):
139 139 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
140 140
141 141
142 142 def pre_pull(ui, repo, **kwargs):
143 143 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
144 144
145 145
146 146 def post_pull(ui, repo, **kwargs):
147 147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
148 148
149 149
150 def _rev_range_hash(repo, node):
151
152 commits = []
153 for rev in xrange(repo[node], len(repo)):
154 ctx = repo[rev]
155 commit_id = mercurial.node.hex(ctx.node())
156 branch = ctx.branch()
157 commits.append((commit_id, branch))
158
159 return commits
160
161
150 162 def pre_push(ui, repo, node=None, **kwargs):
151 163 extras = _extras_from_ui(ui)
152 164
153 165 rev_data = []
154 166 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
155 167 branches = collections.defaultdict(list)
156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
168 for commit_id, branch in _rev_range_hash(repo, node):
157 169 branches[branch].append(commit_id)
158 170
159 171 for branch, commits in branches.iteritems():
160 172 old_rev = kwargs.get('node_last') or commits[0]
161 173 rev_data.append({
162 174 'old_rev': old_rev,
163 175 'new_rev': commits[-1],
164 176 'ref': '',
165 177 'type': 'branch',
166 178 'name': branch,
167 179 })
168 180
169 181 extras['commit_ids'] = rev_data
170 182 return _call_hook('pre_push', extras, HgMessageWriter(ui))
171 183
172 184
173 def _rev_range_hash(repo, node, with_branch=False):
185 def post_push(ui, repo, node, **kwargs):
186 extras = _extras_from_ui(ui)
187
188 commit_ids = []
189 branches = []
190 bookmarks = []
191 tags = []
174 192
175 commits = []
176 for rev in xrange(repo[node], len(repo)):
177 ctx = repo[rev]
178 commit_id = mercurial.node.hex(ctx.node())
179 branch = ctx.branch()
180 if with_branch:
181 commits.append((commit_id, branch))
182 else:
183 commits.append(commit_id)
193 for commit_id, branch in _rev_range_hash(repo, node):
194 commit_ids.append(commit_id)
195 if branch not in branches:
196 branches.append(branch)
184 197
185 return commits
186
198 if hasattr(ui, '_rc_pushkey_branches'):
199 bookmarks = ui._rc_pushkey_branches
187 200
188 def post_push(ui, repo, node, **kwargs):
189 commit_ids = _rev_range_hash(repo, node)
190
191 extras = _extras_from_ui(ui)
192 201 extras['commit_ids'] = commit_ids
202 extras['new_refs'] = {
203 'branches': branches,
204 'bookmarks': bookmarks,
205 'tags': tags
206 }
193 207
194 208 return _call_hook('post_push', extras, HgMessageWriter(ui))
195 209
196 210
197 211 def key_push(ui, repo, **kwargs):
198 212 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
199 213 # store new bookmarks in our UI object propagated later to post_push
200 214 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
201 215 return
202 216
203 217 # backward compat
204 218 log_pull_action = post_pull
205 219
206 220 # backward compat
207 221 log_push_action = post_push
208 222
209 223
210 224 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
211 225 """
212 226 Old hook name: keep here for backward compatibility.
213 227
214 228 This is only required when the installed git hooks are not upgraded.
215 229 """
216 230 pass
217 231
218 232
219 233 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
220 234 """
221 235 Old hook name: keep here for backward compatibility.
222 236
223 237 This is only required when the installed git hooks are not upgraded.
224 238 """
225 239 pass
226 240
227 241
228 242 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
229 243
230 244
231 245 def git_pre_pull(extras):
232 246 """
233 247 Pre pull hook.
234 248
235 249 :param extras: dictionary containing the keys defined in simplevcs
236 250 :type extras: dict
237 251
238 252 :return: status code of the hook. 0 for success.
239 253 :rtype: int
240 254 """
241 255 if 'pull' not in extras['hooks']:
242 256 return HookResponse(0, '')
243 257
244 258 stdout = io.BytesIO()
245 259 try:
246 260 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
247 261 except Exception as error:
248 262 status = 128
249 263 stdout.write('ERROR: %s\n' % str(error))
250 264
251 265 return HookResponse(status, stdout.getvalue())
252 266
253 267
254 268 def git_post_pull(extras):
255 269 """
256 270 Post pull hook.
257 271
258 272 :param extras: dictionary containing the keys defined in simplevcs
259 273 :type extras: dict
260 274
261 275 :return: status code of the hook. 0 for success.
262 276 :rtype: int
263 277 """
264 278 if 'pull' not in extras['hooks']:
265 279 return HookResponse(0, '')
266 280
267 281 stdout = io.BytesIO()
268 282 try:
269 283 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
270 284 except Exception as error:
271 285 status = 128
272 286 stdout.write('ERROR: %s\n' % error)
273 287
274 288 return HookResponse(status, stdout.getvalue())
275 289
276 290
277 291 def _parse_git_ref_lines(revision_lines):
278 292 rev_data = []
279 293 for revision_line in revision_lines or []:
280 294 old_rev, new_rev, ref = revision_line.strip().split(' ')
281 295 ref_data = ref.split('/', 2)
282 296 if ref_data[1] in ('tags', 'heads'):
283 297 rev_data.append({
284 298 'old_rev': old_rev,
285 299 'new_rev': new_rev,
286 300 'ref': ref,
287 301 'type': ref_data[1],
288 302 'name': ref_data[2],
289 303 })
290 304 return rev_data
291 305
292 306
293 307 def git_pre_receive(unused_repo_path, revision_lines, env):
294 308 """
295 309 Pre push hook.
296 310
297 311 :param extras: dictionary containing the keys defined in simplevcs
298 312 :type extras: dict
299 313
300 314 :return: status code of the hook. 0 for success.
301 315 :rtype: int
302 316 """
303 317 extras = json.loads(env['RC_SCM_DATA'])
304 318 rev_data = _parse_git_ref_lines(revision_lines)
305 319 if 'push' not in extras['hooks']:
306 320 return 0
307 321 extras['commit_ids'] = rev_data
308 322 return _call_hook('pre_push', extras, GitMessageWriter())
309 323
310 324
311 325 def _run_command(arguments):
312 326 """
313 327 Run the specified command and return the stdout.
314 328
315 329 :param arguments: sequence of program arguments (including the program name)
316 330 :type arguments: list[str]
317 331 """
318 332 # TODO(skreft): refactor this method and all the other similar ones.
319 333 # Probably this should be using subprocessio.
320 334 process = subprocess.Popen(
321 335 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
322 336 stdout, stderr = process.communicate()
323 337
324 338 if process.returncode != 0:
325 339 raise Exception(
326 340 'Command %s exited with exit code %s: stderr:%s' % (
327 341 arguments, process.returncode, stderr))
328 342
329 343 return stdout
330 344
331 345
332 346 def git_post_receive(unused_repo_path, revision_lines, env):
333 347 """
334 348 Post push hook.
335 349
336 350 :param extras: dictionary containing the keys defined in simplevcs
337 351 :type extras: dict
338 352
339 353 :return: status code of the hook. 0 for success.
340 354 :rtype: int
341 355 """
342 356 extras = json.loads(env['RC_SCM_DATA'])
343 357 if 'push' not in extras['hooks']:
344 358 return 0
345 359
346 360 rev_data = _parse_git_ref_lines(revision_lines)
347 361
348 362 git_revs = []
349 363
350 364 # N.B.(skreft): it is ok to just call git, as git before calling a
351 365 # subcommand sets the PATH environment variable so that it point to the
352 366 # correct version of the git executable.
353 367 empty_commit_id = '0' * 40
368 branches = []
369 tags = []
354 370 for push_ref in rev_data:
355 371 type_ = push_ref['type']
372
356 373 if type_ == 'heads':
357 374 if push_ref['old_rev'] == empty_commit_id:
375 # starting new branch case
376 if push_ref['name'] not in branches:
377 branches.append(push_ref['name'])
358 378
359 379 # Fix up head revision if needed
360 380 cmd = ['git', 'show', 'HEAD']
361 381 try:
362 382 _run_command(cmd)
363 383 except Exception:
364 384 cmd = ['git', 'symbolic-ref', 'HEAD',
365 385 'refs/heads/%s' % push_ref['name']]
366 386 print("Setting default branch to %s" % push_ref['name'])
367 387 _run_command(cmd)
368 388
369 389 cmd = ['git', 'for-each-ref', '--format=%(refname)',
370 390 'refs/heads/*']
371 391 heads = _run_command(cmd)
372 392 heads = heads.replace(push_ref['ref'], '')
373 393 heads = ' '.join(head for head in heads.splitlines() if head)
374 394 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
375 395 '--', push_ref['new_rev'], '--not', heads]
376 396 git_revs.extend(_run_command(cmd).splitlines())
377 397 elif push_ref['new_rev'] == empty_commit_id:
378 398 # delete branch case
379 399 git_revs.append('delete_branch=>%s' % push_ref['name'])
380 400 else:
401 if push_ref['name'] not in branches:
402 branches.append(push_ref['name'])
403
381 404 cmd = ['git', 'log',
382 405 '{old_rev}..{new_rev}'.format(**push_ref),
383 406 '--reverse', '--pretty=format:%H']
384 407 git_revs.extend(_run_command(cmd).splitlines())
385 408 elif type_ == 'tags':
409 if push_ref['name'] not in tags:
410 tags.append(push_ref['name'])
386 411 git_revs.append('tag=>%s' % push_ref['name'])
387 412
388 413 extras['commit_ids'] = git_revs
414 extras['new_refs'] = {
415 'branches': branches,
416 'bookmarks': [],
417 'tags': tags,
418 }
389 419
390 420 if 'repo_size' in extras['hooks']:
391 421 try:
392 422 _call_hook('repo_size', extras, GitMessageWriter())
393 423 except:
394 424 pass
395 425
396 426 return _call_hook('post_push', extras, GitMessageWriter())
@@ -1,239 +1,241 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import contextlib
19 19 import io
20 20 import threading
21 21 from BaseHTTPServer import BaseHTTPRequestHandler
22 22 from SocketServer import TCPServer
23 23
24 24 import mercurial.ui
25 25 import mock
26 26 import pytest
27 27 import simplejson as json
28 28
29 29 from vcsserver import hooks
30 30
31 31
32 32 def get_hg_ui(extras=None):
33 33 """Create a Config object with a valid RC_SCM_DATA entry."""
34 34 extras = extras or {}
35 35 required_extras = {
36 36 'username': '',
37 37 'repository': '',
38 38 'locked_by': '',
39 39 'scm': '',
40 40 'make_lock': '',
41 41 'action': '',
42 42 'ip': '',
43 43 'hooks_uri': 'fake_hooks_uri',
44 44 }
45 45 required_extras.update(extras)
46 46 hg_ui = mercurial.ui.ui()
47 47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
48 48
49 49 return hg_ui
50 50
51 51
52 52 def test_git_pre_receive_is_disabled():
53 53 extras = {'hooks': ['pull']}
54 54 response = hooks.git_pre_receive(None, None,
55 55 {'RC_SCM_DATA': json.dumps(extras)})
56 56
57 57 assert response == 0
58 58
59 59
60 60 def test_git_post_receive_is_disabled():
61 61 extras = {'hooks': ['pull']}
62 62 response = hooks.git_post_receive(None, '',
63 63 {'RC_SCM_DATA': json.dumps(extras)})
64 64
65 65 assert response == 0
66 66
67 67
68 68 def test_git_post_receive_calls_repo_size():
69 69 extras = {'hooks': ['push', 'repo_size']}
70 70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
71 71 hooks.git_post_receive(
72 72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
73 extras.update({'commit_ids': []})
73 extras.update({'commit_ids': [],
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
74 75 expected_calls = [
75 76 mock.call('repo_size', extras, mock.ANY),
76 77 mock.call('post_push', extras, mock.ANY),
77 78 ]
78 79 assert call_hook_mock.call_args_list == expected_calls
79 80
80 81
81 82 def test_git_post_receive_does_not_call_disabled_repo_size():
82 83 extras = {'hooks': ['push']}
83 84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
84 85 hooks.git_post_receive(
85 86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
86 extras.update({'commit_ids': []})
87 extras.update({'commit_ids': [],
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
87 89 expected_calls = [
88 90 mock.call('post_push', extras, mock.ANY)
89 91 ]
90 92 assert call_hook_mock.call_args_list == expected_calls
91 93
92 94
93 95 def test_repo_size_exception_does_not_affect_git_post_receive():
94 96 extras = {'hooks': ['push', 'repo_size']}
95 97 status = 0
96 98
97 99 def side_effect(name, *args, **kwargs):
98 100 if name == 'repo_size':
99 101 raise Exception('Fake exception')
100 102 else:
101 103 return status
102 104
103 105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
104 106 call_hook_mock.side_effect = side_effect
105 107 result = hooks.git_post_receive(
106 108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
107 109 assert result == status
108 110
109 111
110 112 def test_git_pre_pull_is_disabled():
111 113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
112 114
113 115
114 116 def test_git_post_pull_is_disabled():
115 117 assert (
116 118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
117 119
118 120
119 121 class TestGetHooksClient(object):
120 122
121 123 def test_returns_http_client_when_protocol_matches(self):
122 124 hooks_uri = 'localhost:8000'
123 125 result = hooks._get_hooks_client({
124 126 'hooks_uri': hooks_uri,
125 127 'hooks_protocol': 'http'
126 128 })
127 129 assert isinstance(result, hooks.HooksHttpClient)
128 130 assert result.hooks_uri == hooks_uri
129 131
130 132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
131 133 fake_module = mock.Mock()
132 134 import_patcher = mock.patch.object(
133 135 hooks.importlib, 'import_module', return_value=fake_module)
134 136 fake_module_name = 'fake.module'
135 137 with import_patcher as import_mock:
136 138 result = hooks._get_hooks_client(
137 139 {'hooks_module': fake_module_name})
138 140
139 141 import_mock.assert_called_once_with(fake_module_name)
140 142 assert isinstance(result, hooks.HooksDummyClient)
141 143 assert result._hooks_module == fake_module
142 144
143 145
144 146 class TestHooksHttpClient(object):
145 147 def test_init_sets_hooks_uri(self):
146 148 uri = 'localhost:3000'
147 149 client = hooks.HooksHttpClient(uri)
148 150 assert client.hooks_uri == uri
149 151
150 152 def test_serialize_returns_json_string(self):
151 153 client = hooks.HooksHttpClient('localhost:3000')
152 154 hook_name = 'test'
153 155 extras = {
154 156 'first': 1,
155 157 'second': 'two'
156 158 }
157 159 result = client._serialize(hook_name, extras)
158 160 expected_result = json.dumps({
159 161 'method': hook_name,
160 162 'extras': extras
161 163 })
162 164 assert result == expected_result
163 165
164 166 def test_call_queries_http_server(self, http_mirror):
165 167 client = hooks.HooksHttpClient(http_mirror.uri)
166 168 hook_name = 'test'
167 169 extras = {
168 170 'first': 1,
169 171 'second': 'two'
170 172 }
171 173 result = client(hook_name, extras)
172 174 expected_result = {
173 175 'method': hook_name,
174 176 'extras': extras
175 177 }
176 178 assert result == expected_result
177 179
178 180
179 181 class TestHooksDummyClient(object):
180 182 def test_init_imports_hooks_module(self):
181 183 hooks_module_name = 'rhodecode.fake.module'
182 184 hooks_module = mock.MagicMock()
183 185
184 186 import_patcher = mock.patch.object(
185 187 hooks.importlib, 'import_module', return_value=hooks_module)
186 188 with import_patcher as import_mock:
187 189 client = hooks.HooksDummyClient(hooks_module_name)
188 190 import_mock.assert_called_once_with(hooks_module_name)
189 191 assert client._hooks_module == hooks_module
190 192
191 193 def test_call_returns_hook_result(self):
192 194 hooks_module_name = 'rhodecode.fake.module'
193 195 hooks_module = mock.MagicMock()
194 196 import_patcher = mock.patch.object(
195 197 hooks.importlib, 'import_module', return_value=hooks_module)
196 198 with import_patcher:
197 199 client = hooks.HooksDummyClient(hooks_module_name)
198 200
199 201 result = client('post_push', {})
200 202 hooks_module.Hooks.assert_called_once_with()
201 203 assert result == hooks_module.Hooks().__enter__().post_push()
202 204
203 205
204 206 @pytest.fixture
205 207 def http_mirror(request):
206 208 server = MirrorHttpServer()
207 209 request.addfinalizer(server.stop)
208 210 return server
209 211
210 212
211 213 class MirrorHttpHandler(BaseHTTPRequestHandler):
212 214 def do_POST(self):
213 215 length = int(self.headers['Content-Length'])
214 216 body = self.rfile.read(length).decode('utf-8')
215 217 self.send_response(200)
216 218 self.end_headers()
217 219 self.wfile.write(body)
218 220
219 221
220 222 class MirrorHttpServer(object):
221 223 ip_address = '127.0.0.1'
222 224 port = 0
223 225
224 226 def __init__(self):
225 227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
226 228 _, self.port = self._daemon.server_address
227 229 self._thread = threading.Thread(target=self._daemon.serve_forever)
228 230 self._thread.daemon = True
229 231 self._thread.start()
230 232
231 233 def stop(self):
232 234 self._daemon.shutdown()
233 235 self._thread.join()
234 236 self._daemon = None
235 237 self._thread = None
236 238
237 239 @property
238 240 def uri(self):
239 241 return '{}:{}'.format(self.ip_address, self.port)
General Comments 0
You need to be logged in to leave comments. Login now