Path: blob/trunk/py/selenium/webdriver/common/utils.py
4049 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.1617"""Utility functions."""1819import json20import socket21import urllib.request22from collections.abc import Iterable2324from selenium.webdriver.common.keys import Keys2526_is_connectable_exceptions = (socket.error, ConnectionResetError)272829def free_port() -> int:30"""Determines a free port using sockets.3132First try IPv4, but use IPv6 if it can't bind (IPv6-only system).33"""34free_socket = None35try:36# IPv437free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)38free_socket.bind(("127.0.0.1", 0))39except OSError:40if free_socket:41free_socket.close()42# IPv643try:44free_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)45free_socket.bind(("::1", 0))46except OSError:47if free_socket:48free_socket.close()49raise RuntimeError("Can't find free port (Unable to bind to IPv4 or IPv6)")50try:51port: int = free_socket.getsockname()[1]52except Exception as e:53raise RuntimeError(f"Can't find free port: ({e})")54finally:55free_socket.close()56return port575859def find_connectable_ip(host: str | bytes | bytearray | None, port: int | None = None) -> str | None:60"""Resolve a hostname to an IP, preferring IPv4 addresses.6162We prefer IPv4 so that we don't change behavior from previous IPv4-only63implementations, and because some drivers (e.g., FirefoxDriver) do not64support IPv6 connections.6566If the optional port number is provided, only IPs that listen on the given67port are considered.6869Args:70host: hostname71port: port number7273Returns:74A single IP address, as a string. If any IPv4 address is found, one is75returned. Otherwise, if any IPv6 address is found, one is returned. If76neither, then None is returned.77"""78try:79addrinfos = socket.getaddrinfo(host, None)80except socket.gaierror:81return None8283ip = None84for family, _, _, _, sockaddr in addrinfos:85connectable = True86if port:87connectable = is_connectable(port, str(sockaddr[0]))8889if connectable and family == socket.AF_INET:90return str(sockaddr[0])91if connectable and not ip and family == socket.AF_INET6:92ip = str(sockaddr[0])93return ip949596def join_host_port(host: str, port: int) -> str:97"""Joins a hostname and port together.9899This is a minimal implementation intended to cope with IPv6 literals. For100example, _join_host_port('::1', 80) == '[::1]:80'.101102Args:103host: hostname or IP104port: port number105"""106if ":" in host and not host.startswith("["):107return f"[{host}]:{port}"108return f"{host}:{port}"109110111def is_connectable(port: int, host: str | None = "localhost") -> bool:112"""Tries to connect to the server at port to see if it is running.113114Args:115port: port number116host: hostname or IP117"""118socket_ = None119try:120socket_ = socket.create_connection((host, port), 1)121result = True122except _is_connectable_exceptions:123result = False124finally:125if socket_:126try:127socket_.shutdown(socket.SHUT_RDWR)128except Exception:129pass130socket_.close()131return result132133134def is_url_connectable(135port: int | str,136host: str = "localhost",137scheme: str = "http",138) -> bool:139"""Send a request to the HTTP server at the /status endpoint to verify connectivity.140141Args:142port: port number143host: hostname or IP144scheme: URL scheme145146Returns:147True if the service is ready to accept new sessions, False otherwise.148"""149try:150# Disable proxy for localhost connections151proxy_handler = urllib.request.ProxyHandler({})152opener = urllib.request.build_opener(proxy_handler)153154request = urllib.request.Request(f"{scheme}://{host}:{port}/status")155with opener.open(request, timeout=1) as res:156if res.getcode() != 200:157return False158159body = res.read().decode("utf-8")160data = json.loads(body)161162# Check top-level and value.ready, some browsers wrap it under 'value', e.g., ChromeDriver163ready = data.get("ready")164if ready is None:165ready = data.get("value", {}).get("ready")166return ready is True167except Exception:168return False169170171def keys_to_typing(value: Iterable[str | int | float]) -> list[str]:172"""Processes the values that will be typed in the element."""173characters: list[str] = []174for val in value:175if isinstance(val, Keys):176# Todo: Does this even work?177characters.append(str(val))178elif isinstance(val, (int, float)):179characters.extend(str(val))180else:181characters.extend(val)182return characters183184185