Skip to content

Commit 75deb48

Browse files
committed
Fixes/enhancements to server interaction.
- Simplify programmatic stopping API. Now we just have one `stop` method. - Fix possible deadlock if server stopped multiple times. - Fix capturing stdout/stderr. Mainly affects not-so-realistic use case to run server and tests in same process.
1 parent c29c7f5 commit 75deb48

File tree

2 files changed

+81
-94
lines changed

2 files changed

+81
-94
lines changed

src/robotremoteserver.py

Lines changed: 61 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
6060
:param port_file: File to write port that is used. ``None`` means
6161
no such file is written.
6262
:param allow_stop: Allow/disallow stopping the server using ``Stop
63-
Remote Server`` keyword and :meth:`stop_serve`
64-
method.
63+
Remote Server`` keyword.
6564
:param serve: When ``True`` starts the server automatically.
6665
When ``False``, server can be started with
6766
:meth:`serve` or :meth:`start` methods.
@@ -88,43 +87,27 @@ def _register_functions(self, server):
8887
server.register_function(self.run_keyword)
8988
server.register_function(self.get_keyword_arguments)
9089
server.register_function(self.get_keyword_documentation)
91-
server.register_function(self.stop_serve, 'stop_remote_server')
90+
server.register_function(self._stop_serve, 'stop_remote_server')
9291

9392
def serve(self, log=True):
9493
"""Start the server and wait for it to finish.
9594
9695
:param log: Log message about startup or not.
9796
98-
Using this requires using ``serve=False`` when creating initializing
99-
the server. Using ``serve=True`` is equal to first using ``serve=False``
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``
100102
and then calling this method. Alternatively :meth:`start` can be used
101103
to start the server on background.
102104
103105
In addition to signals, the server can be stopped with ``Stop Remote
104-
Server`` keyword or by calling :meth:`stop_serve` method, but both
105-
of these can be disabled with ``allow_stop=False`` when the server
106-
is initialized. Calling :meth:`force_stop_serve` stops the server
107-
unconditionally.
106+
Server`` keyword. Using :meth:`stop` method is possible too, but
107+
requires running this method in a thread.
108108
"""
109109
self._server.serve(log=log)
110110

111-
def stop_serve(self, log=True):
112-
"""Stop the server started by :meth:`serve`.
113-
114-
:param log: Log message about stopping or not.
115-
116-
May be disabled with ``allow_stop=False`` when initializing the server.
117-
Use :meth:`force_stop_serve` if that is a problem.
118-
"""
119-
return self._server.stop_serve(log=log)
120-
121-
def force_stop_serve(self, log=True):
122-
"""Stop the server started by :meth:`serve` unconditionally.
123-
124-
:param log: Log message about stopping or not.
125-
"""
126-
return self._server.stop_serve(force=True, log=log)
127-
128111
def start(self, log=False):
129112
"""Start the server on background.
130113
@@ -142,6 +125,9 @@ def stop(self, log=False):
142125
"""
143126
self._server.stop(log=log)
144127

128+
def _stop_serve(self, log=True):
129+
return self._server.stop_serve(remote=True, log=log)
130+
145131
def _log(self, msg, level=None):
146132
if level:
147133
msg = '*%s* %s' % (level.upper(), msg)
@@ -158,7 +144,7 @@ def get_keyword_names(self):
158144

159145
def run_keyword(self, name, args, kwargs=None):
160146
if name == 'stop_remote_server':
161-
return KeywordRunner(self.stop_serve).run_keyword(args, kwargs)
147+
return KeywordRunner(self._stop_serve).run_keyword(args, kwargs)
162148
return self._library.run_keyword(name, args, kwargs)
163149

164150
def get_keyword_arguments(self, name):
@@ -176,41 +162,14 @@ def get_keyword_documentation(self, name):
176162
class StoppableXMLRPCServer(SimpleXMLRPCServer):
177163
allow_reuse_address = True
178164

179-
def __init__(self, host, port, port_file=None, allow_stop_serve=True):
165+
def __init__(self, host, port, port_file=None, allow_remote_stop=True):
180166
SimpleXMLRPCServer.__init__(self, (host, port), logRequests=False,
181167
bind_and_activate=False)
182168
self._port_file = port_file
183169
self._thread = None
184-
self._allow_stop_serve = allow_stop_serve
170+
self._allow_remote_stop = allow_remote_stop
185171
self._stop_serve = None
186-
187-
def start(self, log=False):
188-
self.server_bind()
189-
self.server_activate()
190-
self._thread = threading.Thread(target=self.serve_forever)
191-
self._thread.start()
192-
self._announce_start(log, self._port_file)
193-
194-
def _announce_start(self, log_start, port_file):
195-
self._log('started', log_start)
196-
if port_file:
197-
with open(port_file, 'w') as pf:
198-
pf.write(str(self.server_address[1]))
199-
200-
def stop(self, log=False):
201-
if self._stop_serve:
202-
self.stop_serve(log=log)
203-
return
204-
self.shutdown()
205-
self.server_close()
206-
self._thread.join()
207-
self._thread = None
208-
self._announce_end(log, self._port_file)
209-
210-
def _announce_end(self, log_end, port_file):
211-
self._log('stopped', log_end)
212-
if port_file and os.path.exists(port_file):
213-
os.remove(port_file) # TODO: Document that port file is removed
172+
self._stop_lock = threading.Lock()
214173

215174
def serve(self, log=True):
216175
self._stop_serve = threading.Event()
@@ -224,7 +183,7 @@ def serve(self, log=True):
224183
@contextmanager
225184
def _stop_signals(self):
226185
original = {}
227-
stop = lambda signum, frame: self.stop_serve()
186+
stop = lambda signum, frame: self.stop_serve(remote=False)
228187
try:
229188
for name in 'SIGINT', 'SIGTERM', 'SIGHUP':
230189
if hasattr(signal, name):
@@ -237,17 +196,45 @@ def _stop_signals(self):
237196
for name in original:
238197
signal.signal(getattr(signal, name), original[name])
239198

240-
def stop_serve(self, force=False, log=True):
241-
if not self._thread:
242-
self._log('is not running', log)
243-
return True
244-
if (self._allow_stop_serve or force) and self._stop_serve:
199+
def stop_serve(self, remote=True, log=True):
200+
if (self._allow_remote_stop or not remote) and self._stop_serve:
245201
self._stop_serve.set()
246202
return True
247203
# TODO: Log to __stdout__? WARN?
248204
self._log('does not allow stopping', log)
249205
return False
250206

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)
214+
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+
251238
def _log(self, action, log=True):
252239
if log:
253240
host, port = self.server_address
@@ -370,6 +357,7 @@ def __init__(self, keyword):
370357
self._keyword = keyword
371358

372359
def run_keyword(self, args, kwargs=None):
360+
# TODO: Handle binary in lists/dicts in args/kwargs
373361
args, kwargs = self._handle_binary_args(args, kwargs or {})
374362
result = KeywordResult()
375363
with StandardStreamInterceptor() as interceptor:
@@ -400,18 +388,20 @@ class StandardStreamInterceptor(object):
400388

401389
def __init__(self):
402390
self.output = ''
403-
404-
def __enter__(self):
391+
self.origout = sys.stdout
392+
self.origerr = sys.stderr
405393
sys.stdout = StringIO()
406394
sys.stderr = StringIO()
395+
396+
def __enter__(self):
407397
return self
408398

409399
def __exit__(self, *exc_info):
410400
stdout = sys.stdout.getvalue()
411401
stderr = sys.stderr.getvalue()
412402
close = [sys.stdout, sys.stderr]
413-
sys.stdout = sys.__stdout__
414-
sys.stderr = sys.__stderr__
403+
sys.stdout = self.origout
404+
sys.stderr = self.origerr
415405
for stream in close:
416406
stream.close()
417407
if stdout and stderr:
@@ -528,9 +518,8 @@ def test_remote_server(uri, log=True):
528518
:param log: Log status message or not.
529519
:return ``True`` if server is running, ``False`` otherwise.
530520
"""
531-
server = ServerProxy(uri)
532521
try:
533-
server.get_keyword_names()
522+
ServerProxy(uri).get_keyword_names()
534523
except Exception:
535524
if log:
536525
print('No remote server running at %s.' % uri)
@@ -545,18 +534,17 @@ def stop_remote_server(uri, log=True):
545534
546535
:param uri: Server address.
547536
:param log: Log status message or not.
548-
:return ``True`` if server was stopped or it was not running,
549-
``False`` otherwise.
537+
:return ``True`` if server was stopped or it was not running in
538+
the first place, ``False`` otherwise.
550539
"""
551540
if not test_remote_server(uri, log=False):
552541
if log:
553542
print('No remote server running at %s.' % uri)
554543
return True
555-
server = ServerProxy(uri)
556544
if log:
557545
print('Stopping remote server at %s.' % uri)
558546
args = [] if log else [False]
559-
return server.stop_remote_server(*args)
547+
return ServerProxy(uri).stop_remote_server(*args)
560548

561549

562550
if __name__ == '__main__':

test/utest/test_interactive.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from contextlib import contextmanager
2+
import sys
23
import threading
34
import time
45
import unittest
56

67
from robot.libraries.Remote import Remote
78

8-
from robotremoteserver import (RobotRemoteServer, stop_remote_server,
9-
test_remote_server)
9+
from robotremoteserver import (RobotRemoteServer, StringIO,
10+
stop_remote_server, test_remote_server)
1011

1112

1213
class Library(object):
1314

1415
def kw(self):
16+
print('The message!')
1517
return 42
1618

1719

@@ -25,12 +27,12 @@ def test_serve(self):
2527
with self._server_thread():
2628
uri = self._wait_until_started()
2729
try:
28-
self.assertEqual(Remote(uri).run_keyword('kw', (), None), 42)
30+
self._run_remote_keyword(uri)
2931
finally:
30-
self.assertEqual(self.server.stop_serve(log=False), True)
32+
self.server.stop(log=False)
3133
self._wait_until_stopped(uri)
32-
self.assertEqual(self.server.stop_serve(log=False), True)
3334
self.assertEqual(test_remote_server(uri, log=False), False)
35+
self.server.stop(log=False)
3436

3537
@contextmanager
3638
def _server_thread(self):
@@ -40,7 +42,7 @@ def _server_thread(self):
4042
try:
4143
yield
4244
finally:
43-
self.server.force_stop_serve(log=False)
45+
self.server.stop(log=False)
4446
thread.join()
4547

4648
def _wait_until_started(self, timeout=5):
@@ -51,20 +53,30 @@ def _wait_until_started(self, timeout=5):
5153
time.sleep(0.01)
5254
raise AssertionError('Server did not start in %s seconds.' % timeout)
5355

56+
def _run_remote_keyword(self, uri):
57+
origout = sys.stdout
58+
sys.stdout = StringIO()
59+
try:
60+
self.assertEqual(Remote(uri).run_keyword('kw', (), None), 42)
61+
self.assertEqual(sys.stdout.getvalue(), 'The message!\n')
62+
finally:
63+
sys.stdout.close()
64+
sys.stdout = origout
65+
5466
def _wait_until_stopped(self, uri, timeout=5):
5567
start_time = time.time()
5668
while time.time() < start_time + timeout:
5769
if not test_remote_server(uri, log=False):
5870
return
5971
time.sleep(0.01)
60-
self.server.stop_serve()
72+
self.server.stop()
6173
raise AssertionError('Server did not stop in %s seconds.' % timeout)
6274

6375
def test_start_and_stop(self):
6476
self.server.start()
6577
uri = 'http://%s:%s' % self.server.server_address
6678
try:
67-
self.assertEqual(Remote(uri).run_keyword('kw', (), None), 42)
79+
self._run_remote_keyword(uri)
6880
finally:
6981
self.server.stop()
7082
self.assertEqual(test_remote_server(uri, log=False), False)
@@ -77,19 +89,6 @@ def test_stop_remote_server_works_with_serve(self):
7789
self._wait_until_stopped(uri)
7890
self.assertEqual(stop_remote_server(uri, log=False), True)
7991

80-
def test_stop_remote_server_can_be_disabled_with_serve(self):
81-
self.server = RobotRemoteServer(Library(), port=0, serve=False,
82-
allow_stop=False)
83-
with self._server_thread():
84-
uri = self._wait_until_started()
85-
self.assertEqual(test_remote_server(uri, log=False), True)
86-
self.assertEqual(stop_remote_server(uri, log=False), False)
87-
self.assertEqual(test_remote_server(uri, log=False), True)
88-
self.assertEqual(self.server.stop_serve(log=False), False)
89-
self.assertEqual(test_remote_server(uri, log=False), True)
90-
self.assertEqual(self.server.force_stop_serve(log=False), True)
91-
self._wait_until_stopped(uri)
92-
9392
def test_stop_remote_server_wont_work_with_start(self):
9493
self.server.start()
9594
uri = 'http://%s:%s' % self.server.server_address

0 commit comments

Comments
 (0)