Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| 888 888 d8b | |
| 888 888 Y8P | |
| 888 888 | |
| .d8888b 88888b. 888d888 .d88b. 88888b.d88b. .d88b. .d88888 888d888 888 888 888 .d88b. 888d888 | |
| d88P" 888 "88b 888P" d88""88b 888 "888 "88b d8P Y8b d88" 888 888P" 888 888 888 d8P Y8b 888P" | |
| 888 888 888 888 888 888 888 888 888 88888888 888 888 888 888 Y88 88P 88888888 888 | |
| Y88b. 888 888 888 Y88..88P 888 888 888 Y8b. Y88b 888 888 888 Y8bd8P Y8b. 888 | |
| "Y8888P 888 888 888 "Y88P" 888 888 888 "Y8888 "Y88888 888 888 Y88P "Y8888 888 88888888 | |
| by UltrafunkAmsterdam (https://github.com/ultrafunkamsterdam) | |
| """ | |
| from __future__ import annotations | |
| __version__ = "3.5.5" | |
| import json | |
| import logging | |
| import os | |
| import pathlib | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import time | |
| from weakref import finalize | |
| import selenium.webdriver.chrome.service | |
| import selenium.webdriver.chrome.webdriver | |
| from selenium.webdriver.common.by import By | |
| import selenium.webdriver.chromium.service | |
| import selenium.webdriver.remote.command | |
| import selenium.webdriver.remote.webdriver | |
| from .cdp import CDP | |
| from .dprocess import start_detached | |
| from .options import ChromeOptions | |
| from .patcher import IS_POSIX | |
| from .patcher import Patcher | |
| from .reactor import Reactor | |
| from .webelement import UCWebElement | |
| from .webelement import WebElement | |
| __all__ = ( | |
| "Chrome", | |
| "ChromeOptions", | |
| "Patcher", | |
| "Reactor", | |
| "CDP", | |
| "find_chrome_executable", | |
| ) | |
| logger = logging.getLogger("uc") | |
| logger.setLevel(logging.getLogger().getEffectiveLevel()) | |
| class Chrome(selenium.webdriver.chrome.webdriver.WebDriver): | |
| """ | |
| Controls the ChromeDriver and allows you to drive the browser. | |
| The webdriver file will be downloaded by this module automatically, | |
| you do not need to specify this. however, you may if you wish. | |
| Attributes | |
| ---------- | |
| Methods | |
| ------- | |
| reconnect() | |
| this can be useful in case of heavy detection methods | |
| -stops the chromedriver service which runs in the background | |
| -starts the chromedriver service which runs in the background | |
| -recreate session | |
| start_session(capabilities=None, browser_profile=None) | |
| differentiates from the regular method in that it does not | |
| require a capabilities argument. The capabilities are automatically | |
| recreated from the options at creation time. | |
| -------------------------------------------------------------------------- | |
| NOTE: | |
| Chrome has everything included to work out of the box. | |
| it does not `need` customizations. | |
| any customizations MAY lead to trigger bot migitation systems. | |
| -------------------------------------------------------------------------- | |
| """ | |
| _instances = set() | |
| session_id = None | |
| debug = False | |
| def __init__( | |
| self, | |
| options=None, | |
| user_data_dir=None, | |
| driver_executable_path=None, | |
| browser_executable_path=None, | |
| port=0, | |
| enable_cdp_events=False, | |
| # service_args=None, | |
| # service_creationflags=None, | |
| desired_capabilities=None, | |
| advanced_elements=False, | |
| # service_log_path=None, | |
| keep_alive=True, | |
| log_level=0, | |
| headless=False, | |
| version_main=None, | |
| patcher_force_close=False, | |
| suppress_welcome=True, | |
| use_subprocess=False, | |
| debug=False, | |
| no_sandbox=True, | |
| windows_headless=False, | |
| user_multi_procs: bool = False, | |
| **kw, | |
| ): | |
| """ | |
| Creates a new instance of the chrome driver. | |
| Starts the service and then creates new instance of chrome driver. | |
| Parameters | |
| ---------- | |
| options: ChromeOptions, optional, default: None - automatic useful defaults | |
| this takes an instance of ChromeOptions, mainly to customize browser behavior. | |
| anything other dan the default, for example extensions or startup options | |
| are not supported in case of failure, and can probably lowers your undetectability. | |
| user_data_dir: str , optional, default: None (creates temp profile) | |
| if user_data_dir is a path to a valid chrome profile directory, use it, | |
| and turn off automatic removal mechanism at exit. | |
| driver_executable_path: str, optional, default: None(=downloads and patches new binary) | |
| browser_executable_path: str, optional, default: None - use find_chrome_executable | |
| Path to the browser executable. | |
| If not specified, make sure the executable's folder is in $PATH | |
| port: int, optional, default: 0 | |
| port to be used by the chromedriver executable, this is NOT the debugger port. | |
| leave it at 0 unless you know what you are doing. | |
| the default value of 0 automatically picks an available port. | |
| enable_cdp_events: bool, default: False | |
| :: currently for chrome only | |
| this enables the handling of wire messages | |
| when enabled, you can subscribe to CDP events by using: | |
| driver.add_cdp_listener("Network.dataReceived", yourcallback) | |
| # yourcallback is an callable which accepts exactly 1 dict as parameter | |
| service_args: list of str, optional, default: None | |
| arguments to pass to the driver service | |
| desired_capabilities: dict, optional, default: None - auto from config | |
| Dictionary object with non-browser specific capabilities only, such as "item" or "loggingPref". | |
| advanced_elements: bool, optional, default: False | |
| makes it easier to recognize elements like you know them from html/browser inspection, especially when working | |
| in an interactive environment | |
| default webelement repr: | |
| <selenium.webdriver.remote.webelement.WebElement (session="85ff0f671512fa535630e71ee951b1f2", element="6357cb55-92c3-4c0f-9416-b174f9c1b8c4")> | |
| advanced webelement repr | |
| <WebElement(<a class="mobile-show-inline-block mc-update-infos init-ok" href="#" id="main-cat-switcher-mobile">)> | |
| note: when retrieving large amounts of elements ( example: find_elements_by_tag("*") ) and print them, it does take a little more time. | |
| service_log_path: str, optional, default: None | |
| path to log information from the driver. | |
| keep_alive: bool, optional, default: True | |
| Whether to configure ChromeRemoteConnection to use HTTP keep-alive. | |
| log_level: int, optional, default: adapts to python global log level | |
| headless: bool, optional, default: False | |
| can also be specified in the options instance. | |
| Specify whether you want to use the browser in headless mode. | |
| warning: this lowers undetectability and not fully supported. | |
| version_main: int, optional, default: None (=auto) | |
| if you, for god knows whatever reason, use | |
| an older version of Chrome. You can specify it's full rounded version number | |
| here. Example: 87 for all versions of 87 | |
| patcher_force_close: bool, optional, default: False | |
| instructs the patcher to do whatever it can to access the chromedriver binary | |
| if the file is locked, it will force shutdown all instances. | |
| setting it is not recommended, unless you know the implications and think | |
| you might need it. | |
| suppress_welcome: bool, optional , default: True | |
| a "welcome" alert might show up on *nix-like systems asking whether you want to set | |
| chrome as your default browser, and if you want to send even more data to google. | |
| now, in case you are nag-fetishist, or a diagnostics data feeder to google, you can set this to False. | |
| Note: if you don't handle the nag screen in time, the browser loses it's connection and throws an Exception. | |
| use_subprocess: bool, optional , default: True, | |
| False (the default) makes sure Chrome will get it's own process (so no subprocess of chromedriver.exe or python | |
| This fixes a LOT of issues, like multithreaded run, but mst importantly. shutting corectly after | |
| program exits or using .quit() | |
| you should be knowing what you're doing, and know how python works. | |
| unfortunately, there is always an edge case in which one would like to write an single script with the only contents being: | |
| --start script-- | |
| import undetected_chromedriver as uc | |
| d = uc.Chrome() | |
| d.get('https://somesite/') | |
| ---end script -- | |
| and will be greeted with an error, since the program exists before chrome has a change to launch. | |
| in that case you can set this to `True`. The browser will start via subprocess, and will keep running most of times. | |
| ! setting it to True comes with NO support when being detected. ! | |
| no_sandbox: bool, optional, default=True | |
| uses the --no-sandbox option, and additionally does suppress the "unsecure option" status bar | |
| this option has a default of True since many people seem to run this as root (....) , and chrome does not start | |
| when running as root without using --no-sandbox flag. | |
| user_multi_procs: | |
| set to true when you are using multithreads/multiprocessing | |
| ensures not all processes are trying to modify a binary which is in use by another. | |
| for this to work. YOU MUST HAVE AT LEAST 1 UNDETECTED_CHROMEDRIVER BINARY IN YOUR ROAMING DATA FOLDER. | |
| this requirement can be easily satisfied, by just running this program "normal" and close/kill it. | |
| """ | |
| finalize(self, self._ensure_close, self) | |
| self.debug = debug | |
| self.patcher = Patcher( | |
| executable_path=driver_executable_path, | |
| force=patcher_force_close, | |
| version_main=version_main, | |
| user_multi_procs=user_multi_procs, | |
| ) | |
| # self.patcher.auto(user_multiprocess = user_multi_num_procs) | |
| self.patcher.auto() | |
| # self.patcher = patcher | |
| if not options: | |
| options = ChromeOptions() | |
| try: | |
| if hasattr(options, "_session") and options._session is not None: | |
| # prevent reuse of options, | |
| # as it just appends arguments, not replace them | |
| # you'll get conflicts starting chrome | |
| raise RuntimeError("you cannot reuse the ChromeOptions object") | |
| except AttributeError: | |
| pass | |
| options._session = self | |
| if not options.debugger_address: | |
| debug_port = ( | |
| port | |
| if port != 0 | |
| else selenium.webdriver.common.service.utils.free_port() | |
| ) | |
| debug_host = "127.0.0.1" | |
| options.debugger_address = "%s:%d" % (debug_host, debug_port) | |
| else: | |
| debug_host, debug_port = options.debugger_address.split(":") | |
| debug_port = int(debug_port) | |
| if enable_cdp_events: | |
| options.set_capability( | |
| "goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"} | |
| ) | |
| options.add_argument("--remote-debugging-host=%s" % debug_host) | |
| options.add_argument("--remote-debugging-port=%s" % debug_port) | |
| if user_data_dir: | |
| options.add_argument("--user-data-dir=%s" % user_data_dir) | |
| language, keep_user_data_dir = None, bool(user_data_dir) | |
| # see if a custom user profile is specified in options | |
| for arg in options.arguments: | |
| if any([_ in arg for _ in ("--headless", "headless")]): | |
| options.arguments.remove(arg) | |
| options.headless = True | |
| if "lang" in arg: | |
| m = re.search("(?:--)?lang(?:[ =])?(.*)", arg) | |
| try: | |
| language = m[1] | |
| except IndexError: | |
| logger.debug("will set the language to en-US,en;q=0.9") | |
| language = "en-US,en;q=0.9" | |
| if "user-data-dir" in arg: | |
| m = re.search("(?:--)?user-data-dir(?:[ =])?(.*)", arg) | |
| try: | |
| user_data_dir = m[1] | |
| logger.debug( | |
| "user-data-dir found in user argument %s => %s" % (arg, m[1]) | |
| ) | |
| keep_user_data_dir = True | |
| except IndexError: | |
| logger.debug( | |
| "no user data dir could be extracted from supplied argument %s " | |
| % arg | |
| ) | |
| if not user_data_dir: | |
| # backward compatiblity | |
| # check if an old uc.ChromeOptions is used, and extract the user data dir | |
| if hasattr(options, "user_data_dir") and getattr( | |
| options, "user_data_dir", None | |
| ): | |
| import warnings | |
| warnings.warn( | |
| "using ChromeOptions.user_data_dir might stop working in future versions." | |
| "use uc.Chrome(user_data_dir='/xyz/some/data') in case you need existing profile folder" | |
| ) | |
| options.add_argument("--user-data-dir=%s" % options.user_data_dir) | |
| keep_user_data_dir = True | |
| logger.debug( | |
| "user_data_dir property found in options object: %s" % user_data_dir | |
| ) | |
| else: | |
| user_data_dir = os.path.normpath(tempfile.mkdtemp()) | |
| keep_user_data_dir = False | |
| arg = "--user-data-dir=%s" % user_data_dir | |
| options.add_argument(arg) | |
| logger.debug( | |
| "created a temporary folder in which the user-data (profile) will be stored during this\n" | |
| "session, and added it to chrome startup arguments: %s" % arg | |
| ) | |
| if not language: | |
| try: | |
| import locale | |
| language = locale.getdefaultlocale()[0].replace("_", "-") | |
| except Exception: | |
| pass | |
| if not language: | |
| language = "en-US" | |
| options.add_argument("--lang=%s" % language) | |
| if not options.binary_location: | |
| options.binary_location = ( | |
| browser_executable_path or find_chrome_executable() | |
| ) | |
| if not options.binary_location or not \ | |
| pathlib.Path(options.binary_location).exists(): | |
| raise FileNotFoundError( | |
| "\n---------------------\n" | |
| "Could not determine browser executable." | |
| "\n---------------------\n" | |
| "Make sure your browser is installed in the default location (path).\n" | |
| "If you are sure about the browser executable, you can specify it using\n" | |
| "the `browser_executable_path='{}` parameter.\n\n" | |
| .format("/path/to/browser/executable" if IS_POSIX else "c:/path/to/your/browser.exe") | |
| ) | |
| self._delay = 3 | |
| self.user_data_dir = user_data_dir | |
| self.keep_user_data_dir = keep_user_data_dir | |
| if suppress_welcome: | |
| options.arguments.extend(["--no-default-browser-check", "--no-first-run"]) | |
| if no_sandbox: | |
| options.arguments.extend(["--no-sandbox", "--test-type"]) | |
| if headless or getattr(options, 'headless', None): | |
| #workaround until a better checking is found | |
| try: | |
| v_main = int(self.patcher.version_main) if self.patcher.version_main else 108 | |
| if v_main < 108: | |
| options.add_argument("--headless=chrome") | |
| elif v_main >= 108: | |
| options.add_argument("--headless=new") | |
| except: | |
| logger.warning("could not detect version_main." | |
| "therefore, we are assuming it is chrome 108 or higher") | |
| options.add_argument("--headless=new") | |
| options.add_argument("--window-size=1920,1080") | |
| options.add_argument("--start-maximized") | |
| options.add_argument("--no-sandbox") | |
| # fixes "could not connect to chrome" error when running | |
| # on linux using privileged user like root (which i don't recommend) | |
| options.add_argument( | |
| "--log-level=%d" % log_level | |
| or divmod(logging.getLogger().getEffectiveLevel(), 10)[0] | |
| ) | |
| if hasattr(options, "handle_prefs"): | |
| options.handle_prefs(user_data_dir) | |
| # fix exit_type flag to prevent tab-restore nag | |
| try: | |
| with open( | |
| os.path.join(user_data_dir, "Default/Preferences"), | |
| encoding="latin1", | |
| mode="r+", | |
| ) as fs: | |
| config = json.load(fs) | |
| if config["profile"]["exit_type"] is not None: | |
| # fixing the restore-tabs-nag | |
| config["profile"]["exit_type"] = None | |
| fs.seek(0, 0) | |
| json.dump(config, fs) | |
| fs.truncate() # the file might be shorter | |
| logger.debug("fixed exit_type flag") | |
| except Exception as e: | |
| logger.debug("did not find a bad exit_type flag ") | |
| self.options = options | |
| if not desired_capabilities: | |
| desired_capabilities = options.to_capabilities() | |
| if not use_subprocess and not windows_headless: | |
| self.browser_pid = start_detached( | |
| options.binary_location, *options.arguments | |
| ) | |
| else: | |
| startupinfo = None | |
| if os.name == 'nt' and windows_headless: | |
| # STARTUPINFO() is Windows only | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW | |
| browser = subprocess.Popen( | |
| [options.binary_location, *options.arguments], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| close_fds=IS_POSIX, | |
| startupinfo=startupinfo | |
| ) | |
| self.browser_pid = browser.pid | |
| service = selenium.webdriver.chromium.service.ChromiumService( | |
| self.patcher.executable_path | |
| ) | |
| super(Chrome, self).__init__( | |
| service=service, | |
| options=options, | |
| keep_alive=keep_alive, | |
| ) | |
| self.reactor = None | |
| if enable_cdp_events: | |
| if logging.getLogger().getEffectiveLevel() == logging.DEBUG: | |
| logging.getLogger( | |
| "selenium.webdriver.remote.remote_connection" | |
| ).setLevel(20) | |
| reactor = Reactor(self) | |
| reactor.start() | |
| self.reactor = reactor | |
| if advanced_elements: | |
| self._web_element_cls = UCWebElement | |
| else: | |
| self._web_element_cls = WebElement | |
| if headless or getattr(options, 'headless', None): | |
| self._configure_headless() | |
| def _configure_headless(self): | |
| orig_get = self.get | |
| logger.info("setting properties for headless") | |
| def get_wrapped(*args, **kwargs): | |
| if self.execute_script("return navigator.webdriver"): | |
| logger.info("patch navigator.webdriver") | |
| self.execute_cdp_cmd( | |
| "Page.addScriptToEvaluateOnNewDocument", | |
| { | |
| "source": """ | |
| Object.defineProperty(window, "navigator", { | |
| Object.defineProperty(window, "navigator", { | |
| value: new Proxy(navigator, { | |
| has: (target, key) => (key === "webdriver" ? false : key in target), | |
| get: (target, key) => | |
| key === "webdriver" | |
| ? false | |
| : typeof target[key] === "function" | |
| ? target[key].bind(target) | |
| : target[key], | |
| }), | |
| }); | |
| """ | |
| }, | |
| ) | |
| logger.info("patch user-agent string") | |
| self.execute_cdp_cmd( | |
| "Network.setUserAgentOverride", | |
| { | |
| "userAgent": self.execute_script( | |
| "return navigator.userAgent" | |
| ).replace("Headless", "") | |
| }, | |
| ) | |
| self.execute_cdp_cmd( | |
| "Page.addScriptToEvaluateOnNewDocument", | |
| { | |
| "source": """ | |
| Object.defineProperty(navigator, 'maxTouchPoints', {get: () => 1}); | |
| Object.defineProperty(navigator.connection, 'rtt', {get: () => 100}); | |
| // https://github.com/microlinkhq/browserless/blob/master/packages/goto/src/evasions/chrome-runtime.js | |
| window.chrome = { | |
| app: { | |
| isInstalled: false, | |
| InstallState: { | |
| DISABLED: 'disabled', | |
| INSTALLED: 'installed', | |
| NOT_INSTALLED: 'not_installed' | |
| }, | |
| RunningState: { | |
| CANNOT_RUN: 'cannot_run', | |
| READY_TO_RUN: 'ready_to_run', | |
| RUNNING: 'running' | |
| } | |
| }, | |
| runtime: { | |
| OnInstalledReason: { | |
| CHROME_UPDATE: 'chrome_update', | |
| INSTALL: 'install', | |
| SHARED_MODULE_UPDATE: 'shared_module_update', | |
| UPDATE: 'update' | |
| }, | |
| OnRestartRequiredReason: { | |
| APP_UPDATE: 'app_update', | |
| OS_UPDATE: 'os_update', | |
| PERIODIC: 'periodic' | |
| }, | |
| PlatformArch: { | |
| ARM: 'arm', | |
| ARM64: 'arm64', | |
| MIPS: 'mips', | |
| MIPS64: 'mips64', | |
| X86_32: 'x86-32', | |
| X86_64: 'x86-64' | |
| }, | |
| PlatformNaclArch: { | |
| ARM: 'arm', | |
| MIPS: 'mips', | |
| MIPS64: 'mips64', | |
| X86_32: 'x86-32', | |
| X86_64: 'x86-64' | |
| }, | |
| PlatformOs: { | |
| ANDROID: 'android', | |
| CROS: 'cros', | |
| LINUX: 'linux', | |
| MAC: 'mac', | |
| OPENBSD: 'openbsd', | |
| WIN: 'win' | |
| }, | |
| RequestUpdateCheckStatus: { | |
| NO_UPDATE: 'no_update', | |
| THROTTLED: 'throttled', | |
| UPDATE_AVAILABLE: 'update_available' | |
| } | |
| } | |
| } | |
| // https://github.com/microlinkhq/browserless/blob/master/packages/goto/src/evasions/navigator-permissions.js | |
| if (!window.Notification) { | |
| window.Notification = { | |
| permission: 'denied' | |
| } | |
| } | |
| const originalQuery = window.navigator.permissions.query | |
| window.navigator.permissions.__proto__.query = parameters => | |
| parameters.name === 'notifications' | |
| ? Promise.resolve({ state: window.Notification.permission }) | |
| : originalQuery(parameters) | |
| const oldCall = Function.prototype.call | |
| function call() { | |
| return oldCall.apply(this, arguments) | |
| } | |
| Function.prototype.call = call | |
| const nativeToStringFunctionString = Error.toString().replace(/Error/g, 'toString') | |
| const oldToString = Function.prototype.toString | |
| function functionToString() { | |
| if (this === window.navigator.permissions.query) { | |
| return 'function query() { [native code] }' | |
| } | |
| if (this === functionToString) { | |
| return nativeToStringFunctionString | |
| } | |
| return oldCall.call(oldToString, this) | |
| } | |
| // eslint-disable-next-line | |
| Function.prototype.toString = functionToString | |
| """ | |
| }, | |
| ) | |
| return orig_get(*args, **kwargs) | |
| self.get = get_wrapped | |
| # def _get_cdc_props(self): | |
| # return self.execute_script( | |
| # """ | |
| # let objectToInspect = window, | |
| # result = []; | |
| # while(objectToInspect !== null) | |
| # { result = result.concat(Object.getOwnPropertyNames(objectToInspect)); | |
| # objectToInspect = Object.getPrototypeOf(objectToInspect); } | |
| # | |
| # return result.filter(i => i.match(/^([a-zA-Z]){27}(Array|Promise|Symbol)$/ig)) | |
| # """ | |
| # ) | |
| # | |
| # def _hook_remove_cdc_props(self): | |
| # self.execute_cdp_cmd( | |
| # "Page.addScriptToEvaluateOnNewDocument", | |
| # { | |
| # "source": """ | |
| # let objectToInspect = window, | |
| # result = []; | |
| # while(objectToInspect !== null) | |
| # { result = result.concat(Object.getOwnPropertyNames(objectToInspect)); | |
| # objectToInspect = Object.getPrototypeOf(objectToInspect); } | |
| # result.forEach(p => p.match(/^([a-zA-Z]){27}(Array|Promise|Symbol)$/ig) | |
| # &&delete window[p]&&console.log('removed',p)) | |
| # """ | |
| # }, | |
| # ) | |
| def get(self, url): | |
| # if self._get_cdc_props(): | |
| # self._hook_remove_cdc_props() | |
| return super().get(url) | |
| def add_cdp_listener(self, event_name, callback): | |
| if ( | |
| self.reactor | |
| and self.reactor is not None | |
| and isinstance(self.reactor, Reactor) | |
| ): | |
| self.reactor.add_event_handler(event_name, callback) | |
| return self.reactor.handlers | |
| return False | |
| def clear_cdp_listeners(self): | |
| if self.reactor and isinstance(self.reactor, Reactor): | |
| self.reactor.handlers.clear() | |
| def window_new(self): | |
| self.execute( | |
| selenium.webdriver.remote.command.Command.NEW_WINDOW, {"type": "window"} | |
| ) | |
| def tab_new(self, url: str): | |
| """ | |
| this opens a url in a new tab. | |
| apparently, that passes all tests directly! | |
| Parameters | |
| ---------- | |
| url | |
| Returns | |
| ------- | |
| """ | |
| if not hasattr(self, "cdp"): | |
| from .cdp import CDP | |
| cdp = CDP(self.options) | |
| cdp.tab_new(url) | |
| def reconnect(self, timeout=0.1): | |
| try: | |
| self.service.stop() | |
| except Exception as e: | |
| logger.debug(e) | |
| time.sleep(timeout) | |
| try: | |
| self.service.start() | |
| except Exception as e: | |
| logger.debug(e) | |
| try: | |
| self.start_session() | |
| except Exception as e: | |
| logger.debug(e) | |
| def start_session(self, capabilities=None, browser_profile=None): | |
| if not capabilities: | |
| capabilities = self.options.to_capabilities() | |
| super(selenium.webdriver.chrome.webdriver.WebDriver, self).start_session( | |
| capabilities | |
| ) | |
| # super(Chrome, self).start_session(capabilities, browser_profile) | |
| def find_elements_recursive(self, by, value): | |
| """ | |
| find elements in all frames | |
| this is a generator function, which is needed | |
| since if it would return a list of elements, they | |
| will be stale on arrival. | |
| using generator, when the element is returned we are in the correct frame | |
| to use it directly | |
| Args: | |
| by: By | |
| value: str | |
| Returns: Generator[webelement.WebElement] | |
| """ | |
| def search_frame(f=None): | |
| if not f: | |
| # ensure we are on main content frame | |
| self.switch_to.default_content() | |
| else: | |
| self.switch_to.frame(f) | |
| for elem in self.find_elements(by, value): | |
| yield elem | |
| # switch back to main content, otherwise we will get StaleElementReferenceException | |
| self.switch_to.default_content() | |
| # search root frame | |
| for elem in search_frame(): | |
| yield elem | |
| # get iframes | |
| frames = self.find_elements('css selector', 'iframe') | |
| # search per frame | |
| for f in frames: | |
| for elem in search_frame(f): | |
| yield elem | |
| def quit(self): | |
| try: | |
| self.service.stop() | |
| self.service.process.kill() | |
| self.command_executor.close() | |
| self.service.process.wait(5) | |
| logger.debug("webdriver process ended") | |
| except (AttributeError, RuntimeError, OSError): | |
| pass | |
| try: | |
| self.reactor.event.set() | |
| logger.debug("shutting down reactor") | |
| except AttributeError: | |
| pass | |
| try: | |
| os.kill(self.browser_pid, 15) | |
| logger.debug("gracefully closed browser") | |
| except Exception as e: # noqa | |
| pass | |
| if ( | |
| hasattr(self, "keep_user_data_dir") | |
| and hasattr(self, "user_data_dir") | |
| and not self.keep_user_data_dir | |
| ): | |
| for _ in range(5): | |
| try: | |
| shutil.rmtree(self.user_data_dir, ignore_errors=False) | |
| except FileNotFoundError: | |
| pass | |
| except (RuntimeError, OSError, PermissionError) as e: | |
| logger.debug( | |
| "When removing the temp profile, a %s occured: %s\nretrying..." | |
| % (e.__class__.__name__, e) | |
| ) | |
| else: | |
| logger.debug("successfully removed %s" % self.user_data_dir) | |
| break | |
| try: | |
| time.sleep(0.1) | |
| except OSError: | |
| pass | |
| # dereference patcher, so patcher can start cleaning up as well. | |
| # this must come last, otherwise it will throw 'in use' errors | |
| self.patcher = None | |
| def __getattribute__(self, item): | |
| if not super().__getattribute__("debug"): | |
| return super().__getattribute__(item) | |
| else: | |
| import inspect | |
| original = super().__getattribute__(item) | |
| if inspect.ismethod(original) and not inspect.isclass(original): | |
| def newfunc(*args, **kwargs): | |
| logger.debug( | |
| "calling %s with args %s and kwargs %s\n" | |
| % (original.__qualname__, args, kwargs) | |
| ) | |
| return original(*args, **kwargs) | |
| return newfunc | |
| return original | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| self.service.stop() | |
| time.sleep(self._delay) | |
| self.service.start() | |
| self.start_session() | |
| def __hash__(self): | |
| return hash(self.options.debugger_address) | |
| def __dir__(self): | |
| return object.__dir__(self) | |
| def __del__(self): | |
| try: | |
| self.service.process.kill() | |
| except: # noqa | |
| pass | |
| self.quit() | |
| def _ensure_close(cls, self): | |
| # needs to be a classmethod so finalize can find the reference | |
| logger.info("ensuring close") | |
| if ( | |
| hasattr(self, "service") | |
| and hasattr(self.service, "process") | |
| and hasattr(self.service.process, "kill") | |
| ): | |
| self.service.process.kill() | |
| def find_chrome_executable(): | |
| """ | |
| Finds the chrome, chrome beta, chrome canary, chromium executable | |
| Returns | |
| ------- | |
| executable_path : str | |
| the full file path to found executable | |
| """ | |
| candidates = set() | |
| if IS_POSIX: | |
| for item in os.environ.get("PATH").split(os.pathsep): | |
| for subitem in ( | |
| "google-chrome", | |
| "chromium", | |
| "chromium-browser", | |
| "chrome", | |
| "google-chrome-stable", | |
| ): | |
| candidates.add(os.sep.join((item, subitem))) | |
| if "darwin" in sys.platform: | |
| candidates.update( | |
| [ | |
| "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", | |
| "/Applications/Chromium.app/Contents/MacOS/Chromium", | |
| ] | |
| ) | |
| else: | |
| for item in map( | |
| os.environ.get, | |
| ("PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA", "PROGRAMW6432"), | |
| ): | |
| if item is not None: | |
| for subitem in ( | |
| "Google/Chrome/Application", | |
| ): | |
| candidates.add(os.sep.join((item, subitem, "chrome.exe"))) | |
| for candidate in candidates: | |
| logger.debug('checking if %s exists and is executable' % candidate) | |
| if os.path.exists(candidate) and os.access(candidate, os.X_OK): | |
| logger.debug('found! using %s' % candidate) | |
| return os.path.normpath(candidate) | |