[py] terminate driver service process when start() fails to connect (#17651)

* [py] terminate driver service process when start() fails to connect

* [py] guard service start cleanup against interrupts and cleanup errors

* [py] add timeout to service remote shutdown request
diff --git a/py/selenium/webdriver/common/service.py b/py/selenium/webdriver/common/service.py
index 41d76ff..aef101e 100644
--- a/py/selenium/webdriver/common/service.py
+++ b/py/selenium/webdriver/common/service.py
@@ -108,15 +108,22 @@
         self._start_process(self._path)
 
         count = 0
-        while True:
-            self.assert_process_still_running()
-            if self.is_connectable():
-                break
-            # sleep increasing: 0.01, 0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, 0.5
-            sleep(min(0.01 + 0.05 * count, 0.5))
-            count += 1
-            if count == 70:
-                raise WebDriverException(f"Can not connect to the Service {self._path}")
+        try:
+            while True:
+                self.assert_process_still_running()
+                if self.is_connectable():
+                    break
+                # sleep increasing: 0.01, 0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, 0.5
+                sleep(min(0.01 + 0.05 * count, 0.5))
+                count += 1
+                if count == 70:
+                    raise WebDriverException(f"Can not connect to the Service {self._path}")
+        except BaseException:
+            try:
+                self.stop()
+            except Exception:
+                logger.error("Error stopping service after a failed start.", exc_info=True)
+            raise
 
     def assert_process_still_running(self) -> None:
         """Check if the underlying process is still running."""
@@ -137,8 +144,8 @@
     def send_remote_shutdown_command(self) -> None:
         """Dispatch an HTTP request to the shutdown endpoint to stop the service."""
         try:
-            request.urlopen(f"{self.service_url}/shutdown")
-        except URLError:
+            request.urlopen(f"{self.service_url}/shutdown", timeout=10)
+        except (URLError, TimeoutError):
             return
 
         for _ in range(30):
diff --git a/py/test/unit/selenium/webdriver/common/service_tests.py b/py/test/unit/selenium/webdriver/common/service_tests.py
new file mode 100644
index 0000000..f695b74
--- /dev/null
+++ b/py/test/unit/selenium/webdriver/common/service_tests.py
@@ -0,0 +1,45 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import sys
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common import utils
+from selenium.webdriver.common.service import Service
+
+
+class _UnreachableService(Service):
+    """A driver process that launches successfully but never serves /status."""
+
+    def command_line_args(self):
+        return ["-c", "import time; time.sleep(30)"]
+
+
+def test_start_terminates_process_when_never_connectable(monkeypatch):
+    monkeypatch.setattr("selenium.webdriver.common.service.sleep", lambda _: None)
+
+    service = _UnreachableService(executable_path=sys.executable, port=utils.free_port())
+
+    try:
+        with pytest.raises(WebDriverException):
+            service.start()
+
+        assert service.process.poll() is not None
+    finally:
+        service.stop()