1616from __future__ import print_function
1717
1818from collections import Mapping
19+ from contextlib import contextmanager
1920import inspect
2021import os
2122import re
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,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):
122180class 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
180259def 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 )
0 commit comments