Skip to content

Commit 9446cb8

Browse files
committed
Support starting server on background. #22
Functionality mostly implemented. Some cleanup and documentation proofreading still needed.
1 parent c2f5c8c commit 9446cb8

File tree

3 files changed

+271
-65
lines changed

3 files changed

+271
-65
lines changed

src/robotremoteserver.py

Lines changed: 163 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import print_function
1717

1818
from collections import Mapping
19+
from contextlib import contextmanager
1920
import inspect
2021
import os
2122
import re
@@ -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,34 +59,92 @@ 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 and :meth:`stop_serve`
64+
method.
65+
:param serve: When ``True`` starts the server automatically.
66+
When ``False``, server can be started with
67+
:meth:`serve` or :meth:`start` methods.
6468
"""
65-
self._server = StoppableXMLRPCServer(host, int(port), port_file)
69+
self._server = StoppableXMLRPCServer(host, int(port), port_file,
70+
allow_stop)
6671
self._library = RemoteLibraryFactory(library)
67-
self._allow_stop = allow_stop
6872
self._register_functions(self._server)
69-
self._server.serve()
73+
if serve:
74+
self.serve()
7075

7176
@property
7277
def server_address(self):
78+
"""Server address as a tuple ``(host, port)``."""
7379
return self._server.server_address
7480

81+
@property
82+
def server_port(self):
83+
"""Server port as an integer."""
84+
return self._server.server_address[1]
85+
7586
def _register_functions(self, server):
7687
server.register_function(self.get_keyword_names)
7788
server.register_function(self.run_keyword)
7889
server.register_function(self.get_keyword_arguments)
7990
server.register_function(self.get_keyword_documentation)
80-
server.register_function(self.stop_remote_server)
91+
server.register_function(self.stop_serve, 'stop_remote_server')
92+
93+
def serve(self, stop_with_signals=True, log=True):
94+
"""Start the server and wait for it to finish.
95+
96+
:param stop_with_signals: Controls should INT, TERM and HUP signals be
97+
registered to stop serving. Can be disabled, for example,
98+
if running this method on a thread where registering
99+
signals is not possible.
100+
:param log: Log message about startup or not.
101+
102+
Using this requires using ``serve=False`` when creating initializing
103+
the server. Using ``serve=True`` is equal to first using ``serve=False``
104+
and then calling this method. Alternatively :meth:`start` can be used
105+
to start the server on background.
106+
107+
In addition to signals, the server can be stopped with ``Stop Remote
108+
Server`` keyword or by calling :meth:`stop_serve` method, but both
109+
of these can be disabled with ``allow_stop=False`` when the server
110+
is initialized. Calling :meth:`force_stop_serve` stops the server
111+
unconditionally.
112+
"""
113+
self._server.serve(stop_with_signals, log)
81114

82-
def stop_remote_server(self):
83-
if self._allow_stop:
84-
self._server.stop_serve()
85-
return True
86-
# TODO: Log to __stdout__? WARN?
87-
print('Robot Framework remote server at %s:%s does not allow stopping.'
88-
% self.server_address)
89-
return False
115+
def stop_serve(self, log=True):
116+
"""Stop the server started by :meth:`serve`.
117+
118+
:param log: Log message about stopping or not.
119+
120+
May be disabled with ``allow_stop=False`` when initializing the server.
121+
Use :meth:`force_stop_serve` if that is a problem.
122+
"""
123+
return self._server.stop_serve(log=log)
124+
125+
def force_stop_serve(self, log=True):
126+
"""Stop the server started by :meth:`serve` unconditionally.
127+
128+
:param log: Log message about stopping or not.
129+
"""
130+
return self._server.stop_serve(force=True, log=log)
131+
132+
def start(self, log=False):
133+
"""Start the server on background.
134+
135+
:param log: Log message about startup or not.
136+
137+
Started server can be stopped with :meth:`stop` method. Stopping is
138+
not possible by using signals or ``Stop Remote Server`` keyword.
139+
"""
140+
self._server.start(log=log)
141+
142+
def stop(self, log=False):
143+
"""Start the server.
144+
145+
:param log: Log message about stopping or not.
146+
"""
147+
self._server.stop(log=log)
90148

91149
def _log(self, msg, level=None):
92150
if level:
@@ -104,12 +162,12 @@ def get_keyword_names(self):
104162

105163
def run_keyword(self, name, args, kwargs=None):
106164
if name == 'stop_remote_server':
107-
return KeywordRunner(self.stop_remote_server).run_keyword(args, kwargs)
165+
return KeywordRunner(self.stop_serve).run_keyword(args, kwargs)
108166
return self._library.run_keyword(name, args, kwargs)
109167

110168
def get_keyword_arguments(self, name):
111169
if name == 'stop_remote_server':
112-
return []
170+
return ['log=True']
113171
return self._library.get_keyword_arguments(name)
114172

115173
def get_keyword_documentation(self, name):
@@ -122,12 +180,13 @@ def get_keyword_documentation(self, name):
122180
class StoppableXMLRPCServer(SimpleXMLRPCServer):
123181
allow_reuse_address = True
124182

125-
def __init__(self, host, port, port_file=None):
183+
def __init__(self, host, port, port_file=None, allow_stop_serve=True):
126184
SimpleXMLRPCServer.__init__(self, (host, port), logRequests=False,
127185
bind_and_activate=False)
128186
self._port_file = port_file
129187
self._thread = None
130-
self._stop_server = threading.Event()
188+
self._allow_stop_serve = allow_stop_serve
189+
self._stop_serve = None
131190

132191
def start(self, log=False):
133192
self.server_bind()
@@ -137,44 +196,64 @@ def start(self, log=False):
137196
self._announce_start(log, self._port_file)
138197

139198
def _announce_start(self, log_start, port_file):
140-
# TODO: starting -> started
141-
if log_start:
142-
print('Robot Framework remote server at %s:%s starting.'
143-
% self.server_address)
199+
self._log('started', log_start)
144200
if port_file:
145201
with open(port_file, 'w') as pf:
146202
pf.write(str(self.server_address[1]))
147203

148204
def stop(self, log=False):
205+
if self._stop_serve:
206+
self.stop_serve(log=log)
207+
return
149208
self.shutdown()
150209
self.server_close()
151210
self._thread.join()
211+
self._thread = None
152212
self._announce_end(log, self._port_file)
153213

154214
def _announce_end(self, log_end, port_file):
155-
# TODO: stopping -> stopped
156-
if log_end:
157-
print('Robot Framework remote server at %s:%s stopping.'
158-
% self.server_address)
215+
self._log('stopped', log_end)
159216
if port_file and os.path.exists(port_file):
160217
os.remove(port_file) # TODO: Document that port file is removed
161218

162-
def serve(self, log=True):
163-
self._stop_server.clear()
164-
self._register_signal_handlers() # TODO: use as context manager!
165-
self.start(log)
166-
while not self._stop_server.is_set():
167-
self._stop_server.wait(1)
168-
self.stop(log)
169-
170-
def _register_signal_handlers(self):
219+
def serve(self, stop_with_signals=True, log=True):
220+
self._stop_serve = threading.Event()
221+
with self._stop_signals(stop_with_signals):
222+
self.start(log)
223+
while not self._stop_serve.is_set():
224+
self._stop_serve.wait(1)
225+
self._stop_serve = None
226+
self.stop(log)
227+
228+
@contextmanager
229+
def _stop_signals(self, stop_with_signals=True):
230+
original = {}
231+
handler = lambda signum, frame: self.stop_serve()
171232
for name in 'SIGINT', 'SIGTERM', 'SIGHUP':
172-
if hasattr(signal, name):
173-
signal.signal(getattr(signal, name),
174-
lambda signum, frame: self.stop_serve())
233+
if stop_with_signals and hasattr(signal, name):
234+
original[name] = signal.signal(getattr(signal, name), handler)
235+
try:
236+
yield
237+
finally:
238+
for name in original:
239+
signal.signal(getattr(signal, name), original[name])
240+
241+
def stop_serve(self, force=False, log=True):
242+
if not self._thread:
243+
self._log('is not running', log)
244+
return True
245+
if (self._allow_stop_serve or force) and self._stop_serve:
246+
self._stop_serve.set()
247+
return True
248+
# TODO: Log to __stdout__? WARN?
249+
self._log('does not allow stopping', log)
250+
return False
175251

176-
def stop_serve(self):
177-
self._stop_server.set()
252+
def _log(self, action, log=True):
253+
if log:
254+
host, port = self.server_address
255+
print ('Robot Framework remote server at %s:%s %s.'
256+
% (host, port, action))
178257

179258

180259
def RemoteLibraryFactory(library):
@@ -443,33 +522,54 @@ def set_output(self, output):
443522
self.data['output'] = self._handle_binary_result(output)
444523

445524

446-
if __name__ == '__main__':
525+
def test_remote_server(uri, log=True):
526+
"""Test is remote server running.
447527
448-
def stop(uri):
449-
server = test(uri, log_success=False)
450-
if server is not None:
451-
print('Stopping remote server at %s.' % uri)
452-
server.stop_remote_server()
528+
:param uri: Server address.
529+
:param log: Log status message or not.
530+
:return ``True`` if server is running, ``False`` otherwise.
531+
"""
532+
server = ServerProxy(uri)
533+
try:
534+
server.get_keyword_names()
535+
except Exception:
536+
if log:
537+
print('No remote server running at %s.' % uri)
538+
return False
539+
if log:
540+
print('Remote server running at %s.' % uri)
541+
return True
453542

454-
def test(uri, log_success=True):
455-
server = ServerProxy(uri)
456-
try:
457-
server.get_keyword_names()
458-
except:
543+
544+
def stop_remote_server(uri, log=True):
545+
"""Stop remote server.
546+
547+
:param uri: Server address.
548+
:param log: Log status message or not.
549+
:return ``True`` if server was stopped or it was not running,
550+
``False`` otherwise.
551+
"""
552+
if not test_remote_server(uri, log=False):
553+
if log:
459554
print('No remote server running at %s.' % uri)
460-
return None
461-
if log_success:
462-
print('Remote server running at %s.' % uri)
463-
return server
464-
465-
def parse_args(args):
466-
actions = {'stop': stop, 'test': test}
467-
if not args or len(args) > 2 or args[0] not in actions:
468-
sys.exit('Usage: python -m robotremoteserver test|stop [uri]')
555+
return True
556+
server = ServerProxy(uri)
557+
if log:
558+
print('Stopping remote server at %s.' % uri)
559+
args = [] if log else [False]
560+
return server.stop_remote_server(*args)
561+
562+
563+
if __name__ == '__main__':
564+
565+
def parse_args(script, *args):
566+
actions = {'stop': stop_remote_server, 'test': test_remote_server}
567+
if not (0 < len(args) < 3) or args[0] not in actions:
568+
sys.exit('Usage: %s {test|stop} [uri]' % os.path.basename(script))
469569
uri = args[1] if len(args) == 2 else 'http://127.0.0.1:8270'
470570
if '://' not in uri:
471571
uri = 'http://' + uri
472572
return actions[args[0]], uri
473573

474-
action, uri = parse_args(sys.argv[1:])
574+
action, uri = parse_args(*sys.argv)
475575
action(uri)

test/atest/resource.robot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ Server Should Be Stopped And Correct Messages Logged
5151
[Arguments] ${test logging}=True
5252
${result} = Wait For Process timeout=10s on_timeout=terminate
5353
${expected} = Catenate SEPARATOR=\n
54-
... Robot Framework remote server at 127.0.0.1:${ACTIVE PORT} starting.
55-
... Robot Framework remote server at 127.0.0.1:${ACTIVE PORT} stopping.
54+
... Robot Framework remote server at 127.0.0.1:${ACTIVE PORT} started.
55+
... Robot Framework remote server at 127.0.0.1:${ACTIVE PORT} stopped.
5656
Run Keyword If ${test logging}
5757
... Should Be Equal ${result.stdout} ${expected}
5858
Should Be Equal ${result.rc} ${0}

0 commit comments

Comments
 (0)