From f242c8df5c9f610fe93a0d03dd1cfe52f128ab18 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 1 Nov 2024 12:21:57 -0400 Subject: [PATCH] Update CDP Mode --- examples/cdp_mode/ReadMe.md | 5 + seleniumbase/core/browser_launcher.py | 6 +- seleniumbase/core/sb_cdp.py | 203 ++++++++++++++++++ .../undetected/cdp_driver/cdp_util.py | 125 ++++++++++- 4 files changed, 337 insertions(+), 2 deletions(-) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index bf6ca1ea..bfba01be 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -231,6 +231,7 @@ sb.cdp.find_all(selector) sb.cdp.find_elements_by_text(text, tag_name=None) sb.cdp.select(selector) sb.cdp.select_all(selector) +sb.cdp.find_elements(selector) sb.cdp.click_link(link_text) sb.cdp.tile_windows(windows=None, max_columns=0) sb.cdp.get_all_cookies(*args, **kwargs) @@ -290,6 +291,8 @@ sb.cdp.get_element_attributes(selector) sb.cdp.get_element_html(selector) sb.cdp.set_locale(locale) sb.cdp.set_attributes(selector, attribute, value) +sb.cdp.gui_click_x_y(x, y) +sb.cdp.gui_click_element(selector) sb.cdp.internalize_links() sb.cdp.is_element_present(selector) sb.cdp.is_element_visible(selector) @@ -297,6 +300,8 @@ sb.cdp.assert_element(selector) sb.cdp.assert_element_present(selector) sb.cdp.assert_text(text, selector="html") sb.cdp.assert_exact_text(text, selector="html") +sb.cdp.scroll_down(amount=25) +sb.cdp.scroll_up(amount=25) sb.cdp.save_screenshot(name, folder=None, selector=None) ``` diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 8acd0499..1060016c 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -588,6 +588,7 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.find_elements_by_text = CDPM.find_elements_by_text cdp.select = CDPM.select cdp.select_all = CDPM.select_all + cdp.find_elements = CDPM.find_elements cdp.click_link = CDPM.click_link cdp.tile_windows = CDPM.tile_windows cdp.get_all_cookies = CDPM.get_all_cookies @@ -619,6 +620,8 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.reset_window_size = CDPM.reset_window_size cdp.set_locale = CDPM.set_locale cdp.set_attributes = CDPM.set_attributes + cdp.gui_click_x_y = CDPM.gui_click_x_y + cdp.gui_click_element = CDPM.gui_click_element cdp.internalize_links = CDPM.internalize_links cdp.get_window = CDPM.get_window cdp.get_element_attributes = CDPM.get_element_attributes @@ -655,6 +658,8 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.assert_element_visible = CDPM.assert_element cdp.assert_text = CDPM.assert_text cdp.assert_exact_text = CDPM.assert_exact_text + cdp.scroll_down = CDPM.scroll_down + cdp.scroll_up = CDPM.scroll_up cdp.save_screenshot = CDPM.save_screenshot cdp.page = page # async world cdp.driver = driver.cdp_base # async world @@ -2218,7 +2223,6 @@ def _set_chrome_options( ) ): chrome_options.add_argument("--no-pings") - chrome_options.add_argument("--disable-popup-blocking") chrome_options.add_argument("--homepage=chrome://version/") chrome_options.add_argument("--animation-duration-scale=0") chrome_options.add_argument("--wm-window-animations-disabled") diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 7686fd04..f920764d 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -1,7 +1,9 @@ """Add CDP methods to extend the driver""" +import fasteners import math import os import re +import sys import time from contextlib import suppress from seleniumbase import config as sb_config @@ -239,6 +241,9 @@ class CDPMethods(): self.__slow_mode_pause_if_set() return updated_elements + def find_elements(self, selector, timeout=settings.SMALL_TIMEOUT): + return self.select_all(selector, timeout=timeout) + def click_link(self, link_text): self.find_elements_by_text(link_text, "a")[0].click() @@ -835,6 +840,194 @@ class CDPMethods(): with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(js_code)) + def __verify_pyautogui_has_a_headed_browser(self): + """PyAutoGUI requires a headed browser so that it can + focus on the correct element when performing actions.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + if driver.config.headless: + raise Exception( + "PyAutoGUI can't be used in headless mode!" + ) + + def __install_pyautogui_if_missing(self): + self.__verify_pyautogui_has_a_headed_browser() + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: # Prevent issues with multiple processes + try: + import pyautogui + with suppress(Exception): + use_pyautogui_ver = constants.PyAutoGUI.VER + if pyautogui.__version__ != use_pyautogui_ver: + del pyautogui + shared_utils.pip_install( + "pyautogui", version=use_pyautogui_ver + ) + import pyautogui + except Exception: + print("\nPyAutoGUI required! Installing now...") + shared_utils.pip_install( + "pyautogui", version=constants.PyAutoGUI.VER + ) + try: + import pyautogui + except Exception: + if ( + shared_utils.is_linux() + and (not sb_config.headed or sb_config.xvfb) + and not driver.config.headless + ): + from sbvirtualdisplay import Display + xvfb_width = 1366 + xvfb_height = 768 + if ( + hasattr(sb_config, "_xvfb_width") + and sb_config._xvfb_width + and isinstance(sb_config._xvfb_width, int) + and hasattr(sb_config, "_xvfb_height") + and sb_config._xvfb_height + and isinstance(sb_config._xvfb_height, int) + ): + xvfb_width = sb_config._xvfb_width + xvfb_height = sb_config._xvfb_height + if xvfb_width < 1024: + xvfb_width = 1024 + sb_config._xvfb_width = xvfb_width + if xvfb_height < 768: + xvfb_height = 768 + sb_config._xvfb_height = xvfb_height + with suppress(Exception): + xvfb_display = Display( + visible=True, + size=(xvfb_width, xvfb_height), + backend="xvfb", + use_xauth=True, + ) + xvfb_display.start() + + def __get_configured_pyautogui(self, pyautogui_copy): + if ( + shared_utils.is_linux() + and hasattr(pyautogui_copy, "_pyautogui_x11") + and "DISPLAY" in os.environ.keys() + ): + if ( + hasattr(sb_config, "_pyautogui_x11_display") + and sb_config._pyautogui_x11_display + and hasattr(pyautogui_copy._pyautogui_x11, "_display") + and ( + sb_config._pyautogui_x11_display + == pyautogui_copy._pyautogui_x11._display + ) + ): + pass + else: + import Xlib.display + pyautogui_copy._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui_copy._pyautogui_x11._display + ) + return pyautogui_copy + + def __gui_click_x_y(self, x, y, timeframe=0.25, uc_lock=False): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x < 0 or y < 0 or x > screen_width or y > screen_height: + raise Exception( + "PyAutoGUI cannot click on point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x, y, screen_width, screen_height) + ) + if uc_lock: + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + if timeframe >= 0.25: + time.sleep(0.056) # Wait if moving at human-speed + if "--debug" in sys.argv: + print(" pyautogui.click(%s, %s)" % (x, y)) + pyautogui.click(x=x, y=y) + else: + # Called from a method where the gui_lock is already active + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + if timeframe >= 0.25: + time.sleep(0.056) # Wait if moving at human-speed + if "--debug" in sys.argv: + print(" pyautogui.click(%s, %s)" % (x, y)) + pyautogui.click(x=x, y=y) + + def gui_click_x_y(self, x, y, timeframe=0.25): + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + width_ratio = 1.0 + if ( + shared_utils.is_windows() + and ( + not hasattr(sb_config, "_saved_width_ratio") + or not sb_config._saved_width_ratio + ) + ): + window_rect = self.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + if ( + hasattr(sb_config, "_saved_width_ratio") + and sb_config._saved_width_ratio + ): + width_ratio = sb_config._saved_width_ratio + else: + scr_width = pyautogui.size().width + self.maximize() + win_width = self.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + width_ratio += 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + self.set_window_rect(win_x, win_y, width, height) + self.bring_active_window_to_front() + elif ( + shared_utils.is_windows() + and hasattr(sb_config, "_saved_width_ratio") + and sb_config._saved_width_ratio + ): + width_ratio = sb_config._saved_width_ratio + self.bring_active_window_to_front() + if shared_utils.is_windows(): + x = x * width_ratio + y = y * width_ratio + self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False) + return + self.bring_active_window_to_front() + self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False) + + def gui_click_element(self, selector, timeframe=0.25): + self.__slow_mode_pause_if_set() + x, y = self.get_gui_element_center(selector) + self.__add_light_pause() + self.gui_click_x_y(x, y, timeframe=timeframe) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait()) + def internalize_links(self): """All `target="_blank"` links become `target="_self"`. This prevents those links from opening in a new tab.""" @@ -938,6 +1131,16 @@ class CDPMethods(): % (text, element.text_all, selector) ) + def scroll_down(self, amount=25): + self.loop.run_until_complete( + self.page.scroll_down(amount) + ) + + def scroll_up(self, amount=25): + self.loop.run_until_complete( + self.page.scroll_up(amount) + ) + def save_screenshot(self, name, folder=None, selector=None): filename = name if folder: diff --git a/seleniumbase/undetected/cdp_driver/cdp_util.py b/seleniumbase/undetected/cdp_driver/cdp_util.py index afa65b6b..5eaa63de 100644 --- a/seleniumbase/undetected/cdp_driver/cdp_util.py +++ b/seleniumbase/undetected/cdp_driver/cdp_util.py @@ -1,10 +1,16 @@ """CDP-Driver is based on NoDriver""" from __future__ import annotations import asyncio +import fasteners import logging +import os import time import types import typing +from contextlib import suppress +from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.fixtures import constants from seleniumbase.fixtures import shared_utils from typing import Optional, List, Union, Callable from .element import Element @@ -15,9 +21,120 @@ from .tab import Tab import mycdp as cdp logger = logging.getLogger(__name__) +IS_LINUX = shared_utils.is_linux() T = typing.TypeVar("T") +def __activate_standard_virtual_display(): + from sbvirtualdisplay import Display + width = settings.HEADLESS_START_WIDTH + height = settings.HEADLESS_START_HEIGHT + with suppress(Exception): + _xvfb_display = Display( + visible=0, size=(width, height) + ) + _xvfb_display.start() + sb_config._virtual_display = _xvfb_display + sb_config.headless_active = True + + +def __activate_virtual_display_as_needed( + headless, headed, xvfb, xvfb_metrics +): + """This is only needed on Linux.""" + if IS_LINUX and (not headed or xvfb): + from sbvirtualdisplay import Display + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: # Prevent issues with multiple processes + if not headless: + import Xlib.display + try: + _xvfb_width = None + _xvfb_height = None + if xvfb_metrics: + with suppress(Exception): + metrics_string = xvfb_metrics + metrics_string = metrics_string.replace(" ", "") + metrics_list = metrics_string.split(",")[0:2] + _xvfb_width = int(metrics_list[0]) + _xvfb_height = int(metrics_list[1]) + # The minimum width,height is: 1024,768 + if _xvfb_width < 1024: + _xvfb_width = 1024 + sb_config._xvfb_width = _xvfb_width + if _xvfb_height < 768: + _xvfb_height = 768 + sb_config._xvfb_height = _xvfb_height + xvfb = True + if not _xvfb_width: + _xvfb_width = 1366 + if not _xvfb_height: + _xvfb_height = 768 + _xvfb_display = Display( + visible=True, + size=(_xvfb_width, _xvfb_height), + backend="xvfb", + use_xauth=True, + ) + _xvfb_display.start() + if "DISPLAY" not in os.environ.keys(): + print( + "\nX11 display failed! Will use regular xvfb!" + ) + __activate_standard_virtual_display() + except Exception as e: + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + print("\nX11 display failed! Will use regular xvfb!") + __activate_standard_virtual_display() + return + pyautogui_is_installed = False + try: + import pyautogui + with suppress(Exception): + use_pyautogui_ver = constants.PyAutoGUI.VER + if pyautogui.__version__ != use_pyautogui_ver: + del pyautogui # To get newer ver + shared_utils.pip_install( + "pyautogui", version=use_pyautogui_ver + ) + import pyautogui + pyautogui_is_installed = True + except Exception: + message = ( + "PyAutoGUI is required for UC Mode on Linux! " + "Installing now..." + ) + print("\n" + message) + shared_utils.pip_install( + "pyautogui", version=constants.PyAutoGUI.VER + ) + import pyautogui + pyautogui_is_installed = True + if ( + pyautogui_is_installed + and hasattr(pyautogui, "_pyautogui_x11") + ): + try: + pyautogui._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui._pyautogui_x11._display + ) + except Exception as e: + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + else: + __activate_standard_virtual_display() + + async def start( config: Optional[Config] = None, *, @@ -27,11 +144,14 @@ async def start( guest: Optional[bool] = False, browser_executable_path: Optional[PathLike] = None, browser_args: Optional[List[str]] = None, + xvfb_metrics: Optional[List[str]] = None, # "Width,Height" for Linux sandbox: Optional[bool] = True, lang: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None, - expert: Optional[bool] = None, + xvfb: Optional[int] = None, # Use a special virtual display on Linux + headed: Optional[bool] = None, # Override default Xvfb mode on Linux + expert: Optional[bool] = None, # Open up closed Shadow-root elements **kwargs: Optional[dict], ) -> Browser: """ @@ -73,6 +193,9 @@ async def start( (For example, ensuring shadow-root is always in "open" mode.) :type expert: bool """ + if IS_LINUX and not headless and not headed and not xvfb: + xvfb = True # The default setting on Linux + __activate_virtual_display_as_needed(headless, headed, xvfb, xvfb_metrics) if not config: config = Config( user_data_dir,