diff --git a/undetected_chromedriver/__init__.py b/undetected_chromedriver/__init__.py index 08e7f117..4bae8fa6 100644 --- a/undetected_chromedriver/__init__.py +++ b/undetected_chromedriver/__init__.py @@ -34,7 +34,7 @@ ChromeOptionsV2 = v2.ChromeOptions logger = logging.getLogger(__name__) -__version__ = "3.0.2" +__version__ = "3.0.3" TARGET_VERSION = 0 diff --git a/undetected_chromedriver/options.py b/undetected_chromedriver/options.py index 8801d319..4ec45f55 100644 --- a/undetected_chromedriver/options.py +++ b/undetected_chromedriver/options.py @@ -11,7 +11,7 @@ class ChromeOptions(_ChromeOptions): KEY = "goog:chromeOptions" - session = None + _session = None emulate_touch = True mock_permissions = True mock_chrome_global = False diff --git a/undetected_chromedriver/patcher.py b/undetected_chromedriver/patcher.py index c86cdb6f..d53be59e 100644 --- a/undetected_chromedriver/patcher.py +++ b/undetected_chromedriver/patcher.py @@ -56,27 +56,44 @@ def __init__(self, executable_path=None, force=False, version_main: int = 0): """ self.force = force + self.executable_path = None if not executable_path: - executable_path = os.path.join(self.data_path, self.exe_name) + self.executable_path = os.path.join(self.data_path, self.exe_name) if not IS_POSIX: - if not executable_path[-4:] == ".exe": - executable_path += ".exe" + if executable_path: + if not executable_path[-4:] == ".exe": + executable_path += ".exe" self.zip_path = os.path.join(self.data_path, self.zip_name) - self.executable_path = os.path.abspath(os.path.join(".", executable_path)) + if not executable_path: + self.executable_path = os.path.abspath( + os.path.join(".", self.executable_path) + ) + + self._custom_exe_path = False + if executable_path: + self._custom_exe_path = True + self.executable_path = executable_path self.version_main = version_main self.version_full = None - def auto(self, executable_path=None, force=False, version_main=None): - """ - """ + """""" if executable_path: self.executable_path = executable_path + self._custom_exe_path = True + + if self._custom_exe_path: + ispatched = self.is_binary_patched(self.executable_path) + if not ispatched: + return self.patch_exe() + else: + return + if version_main: self.version_main = version_main if force is True: diff --git a/undetected_chromedriver/v2.py b/undetected_chromedriver/v2.py index bed7dd59..f5bc5259 100644 --- a/undetected_chromedriver/v2.py +++ b/undetected_chromedriver/v2.py @@ -18,18 +18,25 @@ import selenium.webdriver.common.service import selenium.webdriver.remote.webdriver +from .cdp import CDP from .options import ChromeOptions from .patcher import IS_POSIX, Patcher from .reactor import Reactor -from .cdp import CDP -__all__ = ("Chrome", "ChromeOptions", "Patcher", "Reactor", "CDP", "find_chrome_executable") +__all__ = ( + "Chrome", + "ChromeOptions", + "Patcher", + "Reactor", + "CDP", + "find_chrome_executable", +) logger = logging.getLogger("uc") logger.setLevel(logging.getLogger().getEffectiveLevel()) -class Chrome(selenium.webdriver.Chrome): +class Chrome(selenium.webdriver.chrome.webdriver.WebDriver): """ Controls the ChromeDriver and allows you to drive the browser. @@ -67,6 +74,7 @@ class Chrome(selenium.webdriver.Chrome): """ _instances = set() + session_id = None def __init__( self, @@ -129,9 +137,6 @@ def __init__( Specify whether you want to use the browser in headless mode. warning: this lowers undetectability and not fully supported. - emulate_touch: bool, optional, default: False - if set to True, patches window.maxTouchPoints to always return non-zero - delay: int, optional, default: 5 delay in seconds to wait before giving back control. this is used only when using the context manager @@ -139,7 +144,7 @@ def __init__( 5 seconds is a foolproof value. version_main: int, optional, default: None (=auto) - if you, for god knows whatever reason, use + 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 @@ -149,14 +154,20 @@ def __init__( setting it is not recommended, unless you know the implications and think you might need it. """ - patcher = Patcher(executable_path=executable_path, force=patcher_force_close, version_main=version_main) + + patcher = Patcher( + executable_path=executable_path, + force=patcher_force_close, + version_main=version_main, + ) patcher.auto() if not options: options = ChromeOptions() try: - if options.session and options.session is not None: + 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 @@ -164,7 +175,7 @@ def __init__( except AttributeError: pass - options.session = self + options._session = self debug_port = selenium.webdriver.common.service.utils.free_port() debug_host = "127.0.0.1" @@ -250,9 +261,9 @@ def __init__( 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 + # 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] @@ -280,15 +291,16 @@ def __init__( if not desired_capabilities: desired_capabilities = options.to_capabilities() + self.browser = subprocess.Popen( [options.binary_location, *options.arguments], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - close_fds=True, + close_fds=IS_POSIX, ) - super().__init__( + super(Chrome, self).__init__( executable_path=patcher.executable_path, port=port, options=options, @@ -320,10 +332,22 @@ def __init__( reactor.start() self.reactor = reactor - if options.headless: self._configure_headless() + orig_get = self.get + + # def get_wrapped(*args, **kwargs): + + # self.execute_cdp_cmd( + # "Network.setExtraHTTPHeaders", + # {"headers": {"dnt": "1", "cache-control": "no-cache"}}, + # ) + # + # return orig_get(*args, **kwargs) + # + # self.get = get_wrapped + def _configure_headless(self): orig_get = self.get @@ -508,12 +532,12 @@ def add_cdp_listener(self, event_name, callback): 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 tab_new(self, url:str): + def tab_new(self, url: str): """ this opens a url in a new tab. apparently, that passes all tests directly! @@ -526,17 +550,18 @@ def tab_new(self, url:str): ------- """ - if not hasattr(self, 'cdp'): + if not hasattr(self, "cdp"): from .cdp import CDP + self.cdp = CDP(self.options) self.cdp.tab_new(url) - def reconnect(self): + 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: @@ -550,20 +575,20 @@ def reconnect(self): def start_session(self, capabilities=None, browser_profile=None): if not capabilities: capabilities = self.options.to_capabilities() - super().start_session(capabilities, browser_profile) + super(Chrome, self).start_session(capabilities, browser_profile) + def quit(self): logger.debug("closing webdriver") + self.service.process.kill() try: if self.reactor and isinstance(self.reactor, Reactor): self.reactor.event.set() - super().quit() - except Exception: # noqa pass try: logger.debug("killing browser") - self.browser.kill() + self.browser.terminate() self.browser.wait(1) except TimeoutError as e: @@ -571,10 +596,12 @@ def quit(self): except Exception: # noqa pass - if hasattr(self, 'keep_user_data_dir') \ - and not self.keep_user_data_dir \ - or self.keep_user_data_dir is False: - for _ in range(3): + 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: logger.debug("removing profile : %s" % self.user_data_dir) shutil.rmtree(self.user_data_dir, ignore_errors=False) @@ -585,15 +612,16 @@ def quit(self): "permission error. files are still in use/locked. retying..." ) except (RuntimeError, OSError) as e: - logger.debug( - "%s retying..." % e - ) + logger.debug("%s retying..." % e) else: break - time.sleep(.25) + time.sleep(0.1) def __del__(self): - logger.debug("Chrome.__del__") + try: + self.service.process.kill() + except: + pass self.quit() def __enter__(self): @@ -608,23 +636,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): def __hash__(self): return hash(self.options.debugger_address) - def find_elements_by_text(self, text: str): - for elem in self.find_elements_by_css_selector("*"): - try: - if text.lower() in elem.text.lower(): - yield elem - except Exception as e: - logger.debug("find_elements_by_text: %s" % e) - - def find_element_by_text(self, text: str, selector=None): - if not selector: - selector = "*" - for elem in self.find_elements_by_css_selector(selector): - try: - if text.lower() in elem.text.lower(): - return elem - except Exception as e: - logger.debug("find_elements_by_text: {}".format(e)) def find_chrome_executable():