16
16
# under the License.
17
17
import contextlib
18
18
import errno
19
+ import logging
19
20
import os
20
21
import subprocess
21
22
import typing
23
+ from abc import ABC
24
+ from abc import abstractmethod
22
25
from platform import system
23
26
from subprocess import DEVNULL
24
27
from subprocess import PIPE
25
28
from time import sleep
29
+ from urllib import request
30
+ from urllib .error import URLError
26
31
27
32
from selenium .common .exceptions import WebDriverException
28
33
from selenium .webdriver .common import utils
29
34
35
+ log = logging .getLogger (__name__ )
36
+
37
+
30
38
_HAS_NATIVE_DEVNULL = True
31
39
32
40
33
- class Service :
41
+ class Service ( ABC ) :
34
42
def __init__ (
35
43
self ,
36
44
executable : str ,
@@ -46,19 +54,20 @@ def __init__(
46
54
# Default value for every python subprocess: subprocess.Popen(..., creationflags=0)
47
55
self .creation_flags = 0
48
56
self .env = env or os .environ
49
- self .process = None
57
+ self .process : typing . Optional [ subprocess . Popen ] = None
50
58
51
59
@property
52
- def service_url (self ):
60
+ def service_url (self ) -> str :
53
61
"""
54
62
Gets the url of the Service
55
63
"""
56
64
return f"http://{ utils .join_host_port ('localhost' , self .port )} "
57
65
58
- def command_line_args (self ):
66
+ @abstractmethod
67
+ def command_line_args (self ) -> typing .List [str ]:
59
68
raise NotImplementedError ("This method needs to be implemented in a sub class" )
60
69
61
- def start (self ):
70
+ def start (self ) -> None :
62
71
"""
63
72
Starts the Service.
64
73
@@ -109,26 +118,30 @@ def start(self):
109
118
count += 1
110
119
sleep (0.5 )
111
120
if count == 60 :
112
- raise WebDriverException ("Can not connect to the Service %s" % self .path )
121
+ raise WebDriverException (f "Can not connect to the Service { self .path } " )
113
122
114
- def assert_process_still_running (self ):
123
+ def assert_process_still_running (self ) -> None :
124
+ """Check if the underlying process is still running."""
115
125
return_code = self .process .poll ()
116
126
if return_code :
117
127
raise WebDriverException (f"Service { self .path } unexpectedly exited. Status code was: { return_code } " )
118
128
119
- def is_connectable (self ):
129
+ def is_connectable (self ) -> bool :
130
+ """Establishes a socket connection to determine if the service running on
131
+ the port is accessible."""
120
132
return utils .is_connectable (self .port )
121
133
122
- def send_remote_shutdown_command (self ):
123
- from urllib import request
124
- from urllib .error import URLError
125
-
134
+ def send_remote_shutdown_command (self ) -> None :
135
+ """
136
+ Dispatch an HTTP request to the shutdown endpoint for the service in an
137
+ attempt to stop it.
138
+ """
126
139
try :
127
140
request .urlopen (f"{ self .service_url } /shutdown" )
128
141
except URLError :
129
142
return
130
143
131
- for x in range (30 ):
144
+ for _ in range (30 ):
132
145
if not self .is_connectable ():
133
146
break
134
147
sleep (1 )
@@ -142,27 +155,27 @@ def stop(self) -> None:
142
155
# Todo: Be explicit in what we are catching here.
143
156
self .log_file .close ()
144
157
145
- if not self .process :
146
- return
147
-
148
- try :
149
- self .send_remote_shutdown_command ()
150
- except TypeError :
151
- pass
158
+ if self .process is not None :
159
+ with contextlib .suppress (TypeError ):
160
+ self .send_remote_shutdown_command ()
161
+ self ._terminate_process ()
152
162
163
+ def _terminate_process (self ) -> None :
164
+ """Terminate the child process. On POSIX this attempts a graceful
165
+ SIGTERM followed by a SIGKILL, on a Windows OS kill is an alias to
166
+ terminate. Terminating does not raise itself if something has gone
167
+ wrong but (currently) silently ignores errors here."""
153
168
try :
154
- if self .process :
155
- for stream in [self .process .stdin , self .process .stdout , self .process .stderr ]:
156
- try :
157
- stream .close ()
158
- except AttributeError :
159
- pass
160
- self .process .terminate ()
161
- self .process .wait ()
162
- self .process .kill ()
163
- self .process = None
169
+ stdin , stdout , stderr = self .process .stdin , self .process .stdout , self .process .stdout
170
+ for stream in stdin , stdout , stderr :
171
+ with contextlib .suppress (AttributeError ):
172
+ stream .close ()
173
+ self .process .terminate ()
174
+ self .process .wait (60 )
175
+ # Todo: only SIGKILL if necessary; the process may be cleanly exited by now.
176
+ self .process .kill ()
164
177
except OSError :
165
- pass
178
+ log . error ( "Error terminating service process." , exc_info = True )
166
179
167
180
def __del__ (self ) -> None :
168
181
# `subprocess.Popen` doesn't send signal on `__del__`;
0 commit comments