Skip to content

Commit 8d5152e

Browse files
committed
Merge branch 'threading'
2 parents ea625d3 + b324915 commit 8d5152e

File tree

6 files changed

+327
-89
lines changed

6 files changed

+327
-89
lines changed

src/robotremoteserver.py

Lines changed: 195 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
from __future__ import print_function
1717

1818
from collections import Mapping
19-
import errno
19+
from contextlib import contextmanager
2020
import inspect
21+
import os
2122
import re
2223
import signal
23-
import select
2424
import sys
25+
import threading
2526
import traceback
2627

2728
if sys.version_info < (3,):
@@ -45,10 +46,9 @@
4546

4647

4748
class RobotRemoteServer(object):
48-
allow_reuse_address = True
4949

5050
def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
51-
allow_stop=True):
51+
allow_stop=True, serve=True):
5252
"""Configure and start-up remote server.
5353
5454
:param library: Test library instance or module to host.
@@ -59,52 +59,74 @@ def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
5959
a string.
6060
:param port_file: File to write port that is used. ``None`` means
6161
no such file is written.
62-
:param allow_stop: Allow/disallow stopping the server using
63-
``Stop Remote Server`` keyword.
62+
:param allow_stop: Allow/disallow stopping the server using ``Stop
63+
Remote Server`` keyword.
64+
:param serve: When ``True`` starts the server automatically.
65+
When ``False``, server can be started with
66+
:meth:`serve` or :meth:`start` methods.
6467
"""
65-
self._server = StoppableXMLRPCServer(host, int(port))
68+
self._server = StoppableXMLRPCServer(host, int(port), port_file,
69+
allow_stop)
6670
self._library = RemoteLibraryFactory(library)
67-
self._allow_stop = allow_stop
6871
self._register_functions(self._server)
69-
self._register_signal_handlers()
70-
self._announce_start(port_file)
71-
self._server.start()
72+
if serve:
73+
self.serve()
7274

7375
@property
7476
def server_address(self):
77+
"""Server address as a tuple ``(host, port)``."""
7578
return self._server.server_address
7679

80+
@property
81+
def server_port(self):
82+
"""Server port as an integer."""
83+
return self._server.server_address[1]
84+
7785
def _register_functions(self, server):
7886
server.register_function(self.get_keyword_names)
7987
server.register_function(self.run_keyword)
8088
server.register_function(self.get_keyword_arguments)
8189
server.register_function(self.get_keyword_documentation)
82-
server.register_function(self.stop_remote_server)
83-
84-
def _register_signal_handlers(self):
85-
def stop_with_signal(signum, frame):
86-
self._allow_stop = True
87-
self.stop_remote_server()
88-
for name in 'SIGINT', 'SIGTERM', 'SIGHUP':
89-
if hasattr(signal, name):
90-
signal.signal(getattr(signal, name), stop_with_signal)
91-
92-
def _announce_start(self, port_file=None):
93-
host, port = self.server_address
94-
self._log('Robot Framework remote server at %s:%s starting.'
95-
% (host, port))
96-
if port_file:
97-
with open(port_file, 'w') as pf:
98-
pf.write(str(port))
90+
server.register_function(self._stop_serve, 'stop_remote_server')
9991

100-
def stop_remote_server(self):
101-
prefix = 'Robot Framework remote server at %s:%s ' % self.server_address
102-
if self._allow_stop:
103-
self._log(prefix + 'stopping.')
104-
self._server.stop()
105-
return True
106-
self._log(prefix + 'does not allow stopping.', 'WARN')
107-
return False
92+
def serve(self, log=True):
93+
"""Start the server and wait for it to finish.
94+
95+
:param log: Log message about startup or not.
96+
97+
If this method is called in the main thread, automatically registers
98+
signals INT, TERM and HUP to stop the server.
99+
100+
Using this method requires using ``serve=False`` when initializing the
101+
server. Using ``serve=True`` is equal to first using ``serve=False``
102+
and then calling this method. Alternatively :meth:`start` can be used
103+
to start the server on background.
104+
105+
In addition to signals, the server can be stopped with ``Stop Remote
106+
Server`` keyword. Using :meth:`stop` method is possible too, but
107+
requires running this method in a thread.
108+
"""
109+
self._server.serve(log=log)
110+
111+
def start(self, log=False):
112+
"""Start the server on background.
113+
114+
:param log: Log message about startup or not.
115+
116+
Started server can be stopped with :meth:`stop` method. Stopping is
117+
not possible by using signals or ``Stop Remote Server`` keyword.
118+
"""
119+
self._server.start(log=log)
120+
121+
def stop(self, log=False):
122+
"""Start the server.
123+
124+
:param log: Log message about stopping or not.
125+
"""
126+
self._server.stop(log=log)
127+
128+
def _stop_serve(self, log=True):
129+
return self._server.stop_serve(remote=True, log=log)
108130

109131
def _log(self, msg, level=None):
110132
if level:
@@ -122,12 +144,12 @@ def get_keyword_names(self):
122144

123145
def run_keyword(self, name, args, kwargs=None):
124146
if name == 'stop_remote_server':
125-
return KeywordRunner(self.stop_remote_server).run_keyword(args, kwargs)
147+
return KeywordRunner(self._stop_serve).run_keyword(args, kwargs)
126148
return self._library.run_keyword(name, args, kwargs)
127149

128150
def get_keyword_arguments(self, name):
129151
if name == 'stop_remote_server':
130-
return []
152+
return ['log=True']
131153
return self._library.get_keyword_arguments(name)
132154

133155
def get_keyword_documentation(self, name):
@@ -140,24 +162,84 @@ def get_keyword_documentation(self, name):
140162
class StoppableXMLRPCServer(SimpleXMLRPCServer):
141163
allow_reuse_address = True
142164

143-
def __init__(self, host, port):
144-
SimpleXMLRPCServer.__init__(self, (host, port), logRequests=False)
145-
self._shutdown = False
165+
def __init__(self, host, port, port_file=None, allow_remote_stop=True):
166+
SimpleXMLRPCServer.__init__(self, (host, port), logRequests=False,
167+
bind_and_activate=False)
168+
self._port_file = port_file
169+
self._thread = None
170+
self._allow_remote_stop = allow_remote_stop
171+
self._stop_serve = None
172+
self._stop_lock = threading.Lock()
173+
174+
def serve(self, log=True):
175+
self._stop_serve = threading.Event()
176+
with self._stop_signals():
177+
self.start(log)
178+
while not self._stop_serve.is_set():
179+
self._stop_serve.wait(1)
180+
self._stop_serve = None
181+
self.stop(log)
182+
183+
@contextmanager
184+
def _stop_signals(self):
185+
original = {}
186+
stop = lambda signum, frame: self.stop_serve(remote=False)
187+
try:
188+
for name in 'SIGINT', 'SIGTERM', 'SIGHUP':
189+
if hasattr(signal, name):
190+
original[name] = signal.signal(getattr(signal, name), stop)
191+
except ValueError: # Not in main thread
192+
pass
193+
try:
194+
yield
195+
finally:
196+
for name in original:
197+
signal.signal(getattr(signal, name), original[name])
198+
199+
def stop_serve(self, remote=True, log=True):
200+
if (self._allow_remote_stop or not remote) and self._stop_serve:
201+
self._stop_serve.set()
202+
return True
203+
# TODO: Log to __stdout__? WARN?
204+
self._log('does not allow stopping', log)
205+
return False
146206

147-
def start(self):
148-
if hasattr(self, 'timeout'):
149-
self.timeout = 0.5
150-
elif sys.platform.startswith('java'):
151-
self.socket.settimeout(0.5)
152-
while not self._shutdown:
153-
try:
154-
self.handle_request()
155-
except (OSError, select.error) as err:
156-
if err.args[0] != errno.EINTR:
157-
raise
207+
def start(self, log=False):
208+
self.server_bind()
209+
self.server_activate()
210+
self._thread = threading.Thread(target=self.serve_forever)
211+
self._thread.daemon = True
212+
self._thread.start()
213+
self._announce_start(log, self._port_file)
158214

159-
def stop(self):
160-
self._shutdown = True
215+
def _announce_start(self, log_start, port_file):
216+
self._log('started', log_start)
217+
if port_file:
218+
with open(port_file, 'w') as pf:
219+
pf.write(str(self.server_address[1]))
220+
221+
def stop(self, log=False):
222+
if self._stop_serve:
223+
return self.stop_serve(log=log)
224+
with self._stop_lock:
225+
if not self._thread: # already stopped
226+
return
227+
self.shutdown()
228+
self.server_close()
229+
self._thread.join()
230+
self._thread = None
231+
self._announce_stop(log, self._port_file)
232+
233+
def _announce_stop(self, log_end, port_file):
234+
self._log('stopped', log_end)
235+
if port_file and os.path.exists(port_file):
236+
os.remove(port_file) # TODO: Document that port file is removed
237+
238+
def _log(self, action, log=True):
239+
if log:
240+
host, port = self.server_address
241+
print ('Robot Framework remote server at %s:%s %s.'
242+
% (host, port, action))
161243

162244

163245
def RemoteLibraryFactory(library):
@@ -307,7 +389,8 @@ def __init__(self, keyword):
307389
self._keyword = keyword
308390

309391
def run_keyword(self, args, kwargs=None):
310-
args, kwargs = self._handle_binary_args(args, kwargs or {})
392+
args = self._handle_binary(args)
393+
kwargs = self._handle_binary(kwargs or {})
311394
result = KeywordResult()
312395
with StandardStreamInterceptor() as interceptor:
313396
try:
@@ -324,31 +407,37 @@ def run_keyword(self, args, kwargs=None):
324407
result.set_output(interceptor.output)
325408
return result.data
326409

327-
def _handle_binary_args(self, args, kwargs):
328-
args = [self._handle_binary_arg(a) for a in args]
329-
kwargs = dict((k, self._handle_binary_arg(v)) for k, v in kwargs.items())
330-
return args, kwargs
331-
332-
def _handle_binary_arg(self, arg):
333-
return arg if not isinstance(arg, Binary) else arg.data
410+
def _handle_binary(self, arg):
411+
# No need to compare against other iterables or mappings because we
412+
# only get actual lists and dicts over XML-RPC. Binary cannot be
413+
# a dictionary key either.
414+
if isinstance(arg, list):
415+
return [self._handle_binary(item) for item in arg]
416+
if isinstance(arg, dict):
417+
return dict((key, self._handle_binary(arg[key])) for key in arg)
418+
if isinstance(arg, Binary):
419+
return arg.data
420+
return arg
334421

335422

336423
class StandardStreamInterceptor(object):
337424

338425
def __init__(self):
339426
self.output = ''
340-
341-
def __enter__(self):
427+
self.origout = sys.stdout
428+
self.origerr = sys.stderr
342429
sys.stdout = StringIO()
343430
sys.stderr = StringIO()
431+
432+
def __enter__(self):
344433
return self
345434

346435
def __exit__(self, *exc_info):
347436
stdout = sys.stdout.getvalue()
348437
stderr = sys.stderr.getvalue()
349438
close = [sys.stdout, sys.stderr]
350-
sys.stdout = sys.__stdout__
351-
sys.stderr = sys.__stderr__
439+
sys.stdout = self.origout
440+
sys.stderr = self.origerr
352441
for stream in close:
353442
stream.close()
354443
if stdout and stderr:
@@ -458,33 +547,52 @@ def set_output(self, output):
458547
self.data['output'] = self._handle_binary_result(output)
459548

460549

461-
if __name__ == '__main__':
550+
def test_remote_server(uri, log=True):
551+
"""Test is remote server running.
552+
553+
:param uri: Server address.
554+
:param log: Log status message or not.
555+
:return ``True`` if server is running, ``False`` otherwise.
556+
"""
557+
try:
558+
ServerProxy(uri).get_keyword_names()
559+
except Exception:
560+
if log:
561+
print('No remote server running at %s.' % uri)
562+
return False
563+
if log:
564+
print('Remote server running at %s.' % uri)
565+
return True
462566

463-
def stop(uri):
464-
server = test(uri, log_success=False)
465-
if server is not None:
466-
print('Stopping remote server at %s.' % uri)
467-
server.stop_remote_server()
468567

469-
def test(uri, log_success=True):
470-
server = ServerProxy(uri)
471-
try:
472-
server.get_keyword_names()
473-
except:
568+
def stop_remote_server(uri, log=True):
569+
"""Stop remote server.
570+
571+
:param uri: Server address.
572+
:param log: Log status message or not.
573+
:return ``True`` if server was stopped or it was not running in
574+
the first place, ``False`` otherwise.
575+
"""
576+
if not test_remote_server(uri, log=False):
577+
if log:
474578
print('No remote server running at %s.' % uri)
475-
return None
476-
if log_success:
477-
print('Remote server running at %s.' % uri)
478-
return server
479-
480-
def parse_args(args):
481-
actions = {'stop': stop, 'test': test}
482-
if not args or len(args) > 2 or args[0] not in actions:
483-
sys.exit('Usage: python -m robotremoteserver test|stop [uri]')
579+
return True
580+
if log:
581+
print('Stopping remote server at %s.' % uri)
582+
args = [] if log else [False]
583+
return ServerProxy(uri).stop_remote_server(*args)
584+
585+
586+
if __name__ == '__main__':
587+
588+
def parse_args(script, *args):
589+
actions = {'stop': stop_remote_server, 'test': test_remote_server}
590+
if not (0 < len(args) < 3) or args[0] not in actions:
591+
sys.exit('Usage: %s {test|stop} [uri]' % os.path.basename(script))
484592
uri = args[1] if len(args) == 2 else 'http://127.0.0.1:8270'
485593
if '://' not in uri:
486594
uri = 'http://' + uri
487595
return actions[args[0]], uri
488596

489-
action, uri = parse_args(sys.argv[1:])
597+
action, uri = parse_args(*sys.argv)
490598
action(uri)

test/atest/argument_types.robot

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ Dictionary With Non-String Keys
6868
[Documentation] XML-RPC supports only strings as keys so must convert them
6969
{42: 42, True: False, None: None} {'42': 42, 'True': False, '': ''}
7070

71+
Binary in lists and dicts
72+
[Documentation] Binary values should be handled recursively
73+
(b'\x01', [b'\x02', set([u'\x03'])]) [b'\x01', [b'\x02', [b'\x03']]]
74+
{'k1': b'\x01', 'k2': [b'\x02', {'k3': b'\x03'}]}
75+
7176
*** Keywords ***
7277
Argument Should Be Correct
7378
[Arguments] ${argument} ${expected}=

0 commit comments

Comments
 (0)