1616from __future__ import print_function
1717
1818from collections import Mapping
19- import errno
19+ from contextlib import contextmanager
2020import inspect
21+ import os
2122import re
2223import signal
23- import select
2424import sys
25+ import threading
2526import traceback
2627
2728if sys .version_info < (3 ,):
4546
4647
4748class 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):
140162class 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
163245def 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
336423class 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 )
0 commit comments