16
16
from __future__ import print_function
17
17
18
18
from collections import Mapping
19
+ from contextlib import contextmanager
19
20
import inspect
20
21
import os
21
22
import re
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,34 +59,92 @@ 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 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.
64
68
"""
65
- self ._server = StoppableXMLRPCServer (host , int (port ), port_file )
69
+ self ._server = StoppableXMLRPCServer (host , int (port ), port_file ,
70
+ allow_stop )
66
71
self ._library = RemoteLibraryFactory (library )
67
- self ._allow_stop = allow_stop
68
72
self ._register_functions (self ._server )
69
- self ._server .serve ()
73
+ if serve :
74
+ self .serve ()
70
75
71
76
@property
72
77
def server_address (self ):
78
+ """Server address as a tuple ``(host, port)``."""
73
79
return self ._server .server_address
74
80
81
+ @property
82
+ def server_port (self ):
83
+ """Server port as an integer."""
84
+ return self ._server .server_address [1 ]
85
+
75
86
def _register_functions (self , server ):
76
87
server .register_function (self .get_keyword_names )
77
88
server .register_function (self .run_keyword )
78
89
server .register_function (self .get_keyword_arguments )
79
90
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 )
81
114
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 )
90
148
91
149
def _log (self , msg , level = None ):
92
150
if level :
@@ -104,12 +162,12 @@ def get_keyword_names(self):
104
162
105
163
def run_keyword (self , name , args , kwargs = None ):
106
164
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 )
108
166
return self ._library .run_keyword (name , args , kwargs )
109
167
110
168
def get_keyword_arguments (self , name ):
111
169
if name == 'stop_remote_server' :
112
- return []
170
+ return ['log=True' ]
113
171
return self ._library .get_keyword_arguments (name )
114
172
115
173
def get_keyword_documentation (self , name ):
@@ -122,12 +180,13 @@ def get_keyword_documentation(self, name):
122
180
class StoppableXMLRPCServer (SimpleXMLRPCServer ):
123
181
allow_reuse_address = True
124
182
125
- def __init__ (self , host , port , port_file = None ):
183
+ def __init__ (self , host , port , port_file = None , allow_stop_serve = True ):
126
184
SimpleXMLRPCServer .__init__ (self , (host , port ), logRequests = False ,
127
185
bind_and_activate = False )
128
186
self ._port_file = port_file
129
187
self ._thread = None
130
- self ._stop_server = threading .Event ()
188
+ self ._allow_stop_serve = allow_stop_serve
189
+ self ._stop_serve = None
131
190
132
191
def start (self , log = False ):
133
192
self .server_bind ()
@@ -137,44 +196,64 @@ def start(self, log=False):
137
196
self ._announce_start (log , self ._port_file )
138
197
139
198
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 )
144
200
if port_file :
145
201
with open (port_file , 'w' ) as pf :
146
202
pf .write (str (self .server_address [1 ]))
147
203
148
204
def stop (self , log = False ):
205
+ if self ._stop_serve :
206
+ self .stop_serve (log = log )
207
+ return
149
208
self .shutdown ()
150
209
self .server_close ()
151
210
self ._thread .join ()
211
+ self ._thread = None
152
212
self ._announce_end (log , self ._port_file )
153
213
154
214
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 )
159
216
if port_file and os .path .exists (port_file ):
160
217
os .remove (port_file ) # TODO: Document that port file is removed
161
218
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 ()
171
232
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
175
251
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 ))
178
257
179
258
180
259
def RemoteLibraryFactory (library ):
@@ -443,33 +522,54 @@ def set_output(self, output):
443
522
self .data ['output' ] = self ._handle_binary_result (output )
444
523
445
524
446
- if __name__ == '__main__' :
525
+ def test_remote_server (uri , log = True ):
526
+ """Test is remote server running.
447
527
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
453
542
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 :
459
554
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 ))
469
569
uri = args [1 ] if len (args ) == 2 else 'http://127.0.0.1:8270'
470
570
if '://' not in uri :
471
571
uri = 'http://' + uri
472
572
return actions [args [0 ]], uri
473
573
474
- action , uri = parse_args (sys .argv [ 1 :] )
574
+ action , uri = parse_args (* sys .argv )
475
575
action (uri )
0 commit comments