2424from platform import system
2525from subprocess import PIPE
2626from time import sleep
27- from typing import IO
28- from typing import Any
29- from typing import List
30- from typing import Mapping
31- from typing import Optional
32- from typing import Union
33- from typing import cast
27+ from typing import IO , Any , List , Mapping , Optional , Union , cast
3428from urllib import request
3529from urllib .error import URLError
3630
4236
4337
4438class Service (ABC ):
45- """The abstract base class for all service objects. Services typically
46- launch a child program in a new process as an interim process to
47- communicate with a browser.
48-
49- :param executable: install path of the executable.
50- :param port: Port for the service to run on, defaults to 0 where the operating system will decide.
51- :param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file.
52- :param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`.
53- :param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable.
54- """
55-
5639 def __init__ (
5740 self ,
5841 executable_path : Optional [str ] = None ,
@@ -72,7 +55,6 @@ def __init__(
7255 self .log_output = log_output
7356
7457 self .port = port or utils .free_port ()
75- # Default value for every python subprocess: subprocess.Popen(..., creationflags=0)
7658 self .popen_kw = kwargs .pop ("popen_kw" , {})
7759 self .creation_flags = self .popen_kw .pop ("creation_flags" , 0 )
7860 self .env = env or os .environ
@@ -81,12 +63,10 @@ def __init__(
8163
8264 @property
8365 def service_url (self ) -> str :
84- """Gets the url of the Service."""
8566 return f"http://{ utils .join_host_port ('localhost' , self .port )} "
8667
8768 @abstractmethod
8869 def command_line_args (self ) -> List [str ]:
89- """A List of program arguments (excluding the executable)."""
9070 raise NotImplementedError ("This method needs to be implemented in a sub class" )
9171
9272 @property
@@ -98,12 +78,6 @@ def path(self, value: str) -> None:
9878 self ._path = str (value )
9979
10080 def start (self ) -> None :
101- """Starts the Service.
102-
103- :Exceptions:
104- - WebDriverException : Raised either when it can't start the service
105- or when it can't connect to the service
106- """
10781 if self ._path is None :
10882 raise WebDriverException ("Service path cannot be None." )
10983 self ._start_process (self ._path )
@@ -113,26 +87,20 @@ def start(self) -> None:
11387 self .assert_process_still_running ()
11488 if self .is_connectable ():
11589 break
116- # sleep increasing: 0.01, 0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, 0.5
11790 sleep (min (0.01 + 0.05 * count , 0.5 ))
11891 count += 1
11992 if count == 70 :
12093 raise WebDriverException (f"Can not connect to the Service { self ._path } " )
12194
12295 def assert_process_still_running (self ) -> None :
123- """Check if the underlying process is still running."""
12496 return_code = self .process .poll ()
12597 if return_code :
12698 raise WebDriverException (f"Service { self ._path } unexpectedly exited. Status code was: { return_code } " )
12799
128100 def is_connectable (self ) -> bool :
129- """Establishes a socket connection to determine if the service running
130- on the port is accessible."""
131101 return utils .is_connectable (self .port )
132102
133103 def send_remote_shutdown_command (self ) -> None :
134- """Dispatch an HTTP request to the shutdown endpoint for the service in
135- an attempt to stop it."""
136104 try :
137105 request .urlopen (f"{ self .service_url } /shutdown" )
138106 except URLError :
@@ -144,8 +112,6 @@ def send_remote_shutdown_command(self) -> None:
144112 sleep (1 )
145113
146114 def stop (self ) -> None :
147- """Stops the service."""
148-
149115 if self .log_output not in {PIPE , subprocess .DEVNULL }:
150116 if isinstance (self .log_output , IOBase ):
151117 self .log_output .close ()
@@ -161,13 +127,6 @@ def stop(self) -> None:
161127 self ._terminate_process ()
162128
163129 def _terminate_process (self ) -> None :
164- """Terminate the child process.
165-
166- On POSIX this attempts a graceful SIGTERM followed by a SIGKILL,
167- on a Windows OS kill is an alias to terminate. Terminating does
168- not raise itself if something has gone wrong but (currently)
169- silently ignores errors here.
170- """
171130 try :
172131 stdin , stdout , stderr = (
173132 self .process .stdin ,
@@ -192,11 +151,6 @@ def _terminate_process(self) -> None:
192151 logger .error ("Error terminating service process." , exc_info = True )
193152
194153 def __del__ (self ) -> None :
195- # `subprocess.Popen` doesn't send signal on `__del__`;
196- # so we attempt to close the launched process when `__del__`
197- # is triggered.
198- # do not use globals here; interpreter shutdown may have already cleaned them up
199- # and they would be `None`. This goes for anything this method is referencing internally.
200154 try :
201155 self .stop ()
202156 except Exception :
@@ -205,11 +159,18 @@ def __del__(self) -> None:
205159 def _start_process (self , path : str ) -> None :
206160 """Creates a subprocess by executing the command provided.
207161
208- :param cmd : full command to execute
162+ :param path : full command to execute
209163 """
164+ # Pre-check: validate path exists and is executable
165+ if not os .path .isfile (path ):
166+ raise WebDriverException (f"The executable at path '{ path } ' does not exist." )
167+ if not os .access (path , os .X_OK ):
168+ raise WebDriverException (f"The executable at path '{ path } ' is not executable." )
169+
210170 cmd = [path ]
211171 cmd .extend (self .command_line_args ())
212172 close_file_descriptors = self .popen_kw .pop ("close_fds" , system () != "Windows" )
173+
213174 try :
214175 start_info = None
215176 if system () == "Windows" :
@@ -239,8 +200,6 @@ def _start_process(self, path: str) -> None:
239200 raise
240201 except OSError as err :
241202 if err .errno == errno .EACCES :
242- if self ._path is None :
243- raise WebDriverException ("Service path cannot be None." )
244203 raise WebDriverException (
245204 f"'{ os .path .basename (self ._path )} ' executable may have wrong permissions."
246205 ) from err
0 commit comments