16
16
from __future__ import print_function
17
17
18
18
from collections import Mapping
19
- import errno
19
+ from contextlib import contextmanager
20
20
import inspect
21
+ import os
21
22
import re
22
23
import signal
23
- import select
24
24
import sys
25
+ import threading
25
26
import traceback
26
27
27
28
if sys .version_info < (3 ,):
45
46
46
47
47
48
class RobotRemoteServer (object ):
48
- allow_reuse_address = True
49
49
50
50
def __init__ (self , library , host = '127.0.0.1' , port = 8270 , port_file = None ,
51
- allow_stop = True ):
51
+ allow_stop = True , serve = True ):
52
52
"""Configure and start-up remote server.
53
53
54
54
: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,
59
59
a string.
60
60
:param port_file: File to write port that is used. ``None`` means
61
61
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.
64
67
"""
65
- self ._server = StoppableXMLRPCServer (host , int (port ))
68
+ self ._server = StoppableXMLRPCServer (host , int (port ), port_file ,
69
+ allow_stop )
66
70
self ._library = RemoteLibraryFactory (library )
67
- self ._allow_stop = allow_stop
68
71
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 ()
72
74
73
75
@property
74
76
def server_address (self ):
77
+ """Server address as a tuple ``(host, port)``."""
75
78
return self ._server .server_address
76
79
80
+ @property
81
+ def server_port (self ):
82
+ """Server port as an integer."""
83
+ return self ._server .server_address [1 ]
84
+
77
85
def _register_functions (self , server ):
78
86
server .register_function (self .get_keyword_names )
79
87
server .register_function (self .run_keyword )
80
88
server .register_function (self .get_keyword_arguments )
81
89
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' )
99
91
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 )
108
130
109
131
def _log (self , msg , level = None ):
110
132
if level :
@@ -122,12 +144,12 @@ def get_keyword_names(self):
122
144
123
145
def run_keyword (self , name , args , kwargs = None ):
124
146
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 )
126
148
return self ._library .run_keyword (name , args , kwargs )
127
149
128
150
def get_keyword_arguments (self , name ):
129
151
if name == 'stop_remote_server' :
130
- return []
152
+ return ['log=True' ]
131
153
return self ._library .get_keyword_arguments (name )
132
154
133
155
def get_keyword_documentation (self , name ):
@@ -140,24 +162,84 @@ def get_keyword_documentation(self, name):
140
162
class StoppableXMLRPCServer (SimpleXMLRPCServer ):
141
163
allow_reuse_address = True
142
164
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
146
206
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 )
158
214
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 ))
161
243
162
244
163
245
def RemoteLibraryFactory (library ):
@@ -307,7 +389,8 @@ def __init__(self, keyword):
307
389
self ._keyword = keyword
308
390
309
391
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 {})
311
394
result = KeywordResult ()
312
395
with StandardStreamInterceptor () as interceptor :
313
396
try :
@@ -324,31 +407,37 @@ def run_keyword(self, args, kwargs=None):
324
407
result .set_output (interceptor .output )
325
408
return result .data
326
409
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
334
421
335
422
336
423
class StandardStreamInterceptor (object ):
337
424
338
425
def __init__ (self ):
339
426
self .output = ''
340
-
341
- def __enter__ ( self ):
427
+ self . origout = sys . stdout
428
+ self . origerr = sys . stderr
342
429
sys .stdout = StringIO ()
343
430
sys .stderr = StringIO ()
431
+
432
+ def __enter__ (self ):
344
433
return self
345
434
346
435
def __exit__ (self , * exc_info ):
347
436
stdout = sys .stdout .getvalue ()
348
437
stderr = sys .stderr .getvalue ()
349
438
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
352
441
for stream in close :
353
442
stream .close ()
354
443
if stdout and stderr :
@@ -458,33 +547,52 @@ def set_output(self, output):
458
547
self .data ['output' ] = self ._handle_binary_result (output )
459
548
460
549
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
462
566
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 ()
468
567
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 :
474
578
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 ))
484
592
uri = args [1 ] if len (args ) == 2 else 'http://127.0.0.1:8270'
485
593
if '://' not in uri :
486
594
uri = 'http://' + uri
487
595
return actions [args [0 ]], uri
488
596
489
- action , uri = parse_args (sys .argv [ 1 :] )
597
+ action , uri = parse_args (* sys .argv )
490
598
action (uri )
0 commit comments