diff --git a/examples/boilerplates/samples/google_test.py b/examples/boilerplates/samples/google_test.py index 7683d035b58..06c569433cd 100644 --- a/examples/boilerplates/samples/google_test.py +++ b/examples/boilerplates/samples/google_test.py @@ -20,7 +20,7 @@ def test_google_dot_com(self): self.assert_title_contains("Google") self.sleep(0.05) self.save_screenshot_to_logs() # ("./latest_logs" folder) - self.press_keys(HomePage.search_box, "GitHub") + self.type(HomePage.search_box, "GitHub") self.assert_element(HomePage.search_button) self.assert_element(HomePage.feeling_lucky_button) self.click(HomePage.search_button) diff --git a/examples/raw_google.py b/examples/raw_google.py index 869ac0070b3..105772755a2 100644 --- a/examples/raw_google.py +++ b/examples/raw_google.py @@ -4,7 +4,7 @@ sb.activate_cdp_mode() sb.goto("https://google.com/ncr") sb.click_if_visible('button:contains("Accept all")') - sb.press_keys('[name="q"]', "SeleniumBase GitHub page") + sb.type('[name="q"]', "SeleniumBase GitHub page") sb.click('[value="Google Search"]') sb.sleep(4) # The "AI Overview" sometimes loads print(sb.get_page_title()) diff --git a/examples/test_download_files.py b/examples/test_download_files.py index 16d0e805eb2..e62b1207edf 100644 --- a/examples/test_download_files.py +++ b/examples/test_download_files.py @@ -29,7 +29,7 @@ def test_download_files_from_pypi(self): self.assert_element("span#pip-command") self.assert_text("Download files", "div#files h2.page-title") self.assert_text("Download files", "a#files-tab") - pkg_header = self.get_text("h1.package-header__name").strip() + pkg_header = self.get_text('h1[class*="header__name"]').strip() pkg_name = pkg_header.replace(" ", "-") whl_file = pkg_name + "-py3-none-any.whl" tar_gz_file = pkg_name + ".tar.gz" diff --git a/requirements.txt b/requirements.txt index df73d24e15e..91564bec994 100755 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ pytest-xdist==3.8.0 parameterized==0.9.0 behave==1.2.6 soupsieve~=2.8.4;python_version<"3.10" -soupsieve~=2.9.1;python_version>="3.10" +soupsieve~=2.9.2;python_version>="3.10" beautifulsoup4~=4.15.0 pyotp~=2.10.0 python-xlib==0.33;platform_system=="Linux" @@ -82,7 +82,7 @@ rich>=15.0.0,<16 # ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.) coverage>=7.10.7;python_version<"3.10" -coverage>=7.15.3;python_version>="3.10" +coverage>=7.15.4;python_version>="3.10" pytest-cov>=7.1.0 flake8==7.3.0 mccabe==0.7.0 diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 66c9f7777d4..35ecb7dc9c5 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.51.10" +__version__ = "4.51.11" diff --git a/seleniumbase/undetected/__init__.py b/seleniumbase/undetected/__init__.py index e37947d8c3b..b896697ab9c 100644 --- a/seleniumbase/undetected/__init__.py +++ b/seleniumbase/undetected/__init__.py @@ -292,14 +292,15 @@ def __init__( ) browser = subprocess.Popen( [options.binary_location, *options.arguments], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, close_fds=IS_POSIX, creationflags=creationflags, ) self.browser_pid = browser.pid - self._process_pid = browser.pid + self.browser_process = browser + self._process_pid = self.browser_pid try: self._process_create_time = ( psutil.Process(self._process_pid).create_time() @@ -307,7 +308,7 @@ def __init__( except Exception: self._process_create_time = None service_ = None - log_output = subprocess.PIPE + log_output = subprocess.DEVNULL if patch_driver: service_ = selenium.webdriver.chrome.service.Service( executable_path=self.patcher.executable_path, @@ -328,6 +329,16 @@ def __init__( setattr(service_, "creation_flags", creationflags) try: super().__init__(options=options, service=service_) + if ( + hasattr(self, "service") + and getattr(self.service, "process", None) + ): + self._service_process = self.service.process + if ( + self._service_process.stdin + and not self._service_process.stdin.closed + ): + self._service_process.stdin.close() except OSError as e: if IS_MAC and "Bad CPU type in executable" in str(e): print(str(e)) @@ -665,6 +676,16 @@ def __del__(self): super().quit() with suppress(Exception): self.quit() + with suppress(Exception): + if hasattr(self, "_service_process") and self._service_process: + if self._service_process.poll() is None: + self._service_process.kill() + self._service_process.wait(timeout=1) + with suppress(Exception): + if hasattr(self, "browser_process") and self.browser_process: + if self.browser_process.poll() is None: + self.browser_process.kill() + self.browser_process.wait(timeout=1) def __enter__(self): return self diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 4e581d686a9..e0f6bafe837 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -656,9 +656,9 @@ async def start(self=None) -> Browser: await asyncio.create_subprocess_exec( exe, *params, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, close_fds=is_posix, ) ) @@ -934,7 +934,22 @@ def stop(self, deconstruct=False): close_success = False try: if self.connection: - loop = asyncio.get_running_loop() + loop = None + for obj in (self, self.connection, getattr( + self.connection, "websocket", None) + ): + if hasattr(obj, "loop"): + loop = obj.loop + break + if hasattr(obj, "_loop"): + loop = obj._loop + break + if not loop: + with suppress(Exception): + loop = asyncio.get_event_loop_policy().get_event_loop() + if not loop: + with suppress(Exception): + loop = asyncio.get_event_loop() if loop.is_running(): loop.create_task(self.connection.aclose()) logger.debug("Closed connection with create_task()") @@ -952,8 +967,9 @@ def stop(self, deconstruct=False): for _ in range(3): try: if connection_id not in sb_config._closed_connection_ids: + the_proc = psutil.Process(self._process.pid) self._process.terminate() - procs.append(psutil.Process(self._process.pid)) + procs.append(the_proc) logger.debug( "Terminated browser with pid %d successfully." % self._process.pid @@ -964,8 +980,9 @@ def stop(self, deconstruct=False): break except (Exception,): try: + the_proc = psutil.Process(self._process.pid) self._process.kill() - procs.append(psutil.Process(self._process.pid)) + procs.append(the_proc) logger.debug( "Killed browser with pid %d successfully." % self._process.pid @@ -1016,6 +1033,27 @@ def stop(self, deconstruct=False): logger.debug("Process has been terminated: %d." % p.pid) for p in alive: logger.debug("Process is still alive: %d." % p.pid) + if "loop" in locals() and loop and not loop.is_closed(): + with suppress(Exception): + if not loop.is_running(): + # Time to flush websocket closes and child watcher signals + loop.run_until_complete(asyncio.sleep(0.05)) + # Cancel lingering tasks + pending = [ + t for t in asyncio.all_tasks(loop) if not t.done() + ] + for task in pending: + task.cancel() + if pending: + loop.run_until_complete( + asyncio.gather(*pending, return_exceptions=True) + ) + # Shutdown and close + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + # Clear the closed loop from the thread policy + # so future tests get a fresh loop + asyncio.set_event_loop(asyncio.new_event_loop()) if self.config.user_data_dir and not self.config.uses_custom_data_dir: for _ in range(3): try: diff --git a/seleniumbase/undetected/cdp_driver/element.py b/seleniumbase/undetected/cdp_driver/element.py index 3230e25041e..91e240ae7e4 100644 --- a/seleniumbase/undetected/cdp_driver/element.py +++ b/seleniumbase/undetected/cdp_driver/element.py @@ -528,8 +528,8 @@ async def mouse_click_async( with suppress(Exception): await self.mouse_move_async() await asyncio.sleep(random.uniform(0.0036, 0.0046)) - x = center[0] + random.uniform(-0.85, 0.85) - y = center[1] + random.uniform(-0.85, 0.85) + x = center[0] + random.uniform(-0.875, 0.875) + y = center[1] + random.uniform(-0.875, 0.875) asyncio.create_task( self._tab.send( cdp.input_.dispatch_mouse_event( @@ -546,23 +546,23 @@ async def mouse_click_async( ) if not timeframe or timeframe <= 0: # If 0 (or less), hold for a small amount of time - await asyncio.sleep(random.uniform(0.0142, 0.0152)) - x += random.uniform(-0.12, 0.12) - y += random.uniform(-0.12, 0.12) + await asyncio.sleep(random.uniform(0.015, 0.016)) + x += random.uniform(-0.115, 0.115) + y += random.uniform(-0.115, 0.115) else: end_time = asyncio.get_running_loop().time() + timeframe while asyncio.get_running_loop().time() < end_time: await self._tab.send( cdp.input_.dispatch_mouse_event( type_="mouseMoved", - x=x + random.uniform(-0.12, 0.12), - y=y + random.uniform(-0.12, 0.12), + x=x + random.uniform(-0.115, 0.115), + y=y + random.uniform(-0.115, 0.115), button=cdp.input_.MouseButton(button), buttons=buttons, force=0.5, ) ) - await asyncio.sleep(random.uniform(0.120, 0.280)) + await asyncio.sleep(random.uniform(0.15, 0.25)) asyncio.create_task( self._tab.send( cdp.input_.dispatch_mouse_event( @@ -919,6 +919,7 @@ async def send_keys_async(self, text: str): windows_virtual_key_code=vk, ) ) + await asyncio.sleep(random.uniform(0.002, 0.003)) # 3. Trigger keypress DOM event AND insert text if text_val: await self._tab.send( @@ -932,7 +933,7 @@ async def send_keys_async(self, text: str): ) ) # 4. Trigger keyup DOM event - await asyncio.sleep(random.uniform(0.0126, 0.0152)) + await asyncio.sleep(random.uniform(0.011, 0.014)) await self._tab.send( cdp.input_.dispatch_key_event( type_="keyUp", diff --git a/setup.py b/setup.py index 5368cbd182f..5a913ff17eb 100755 --- a/setup.py +++ b/setup.py @@ -232,7 +232,7 @@ 'parameterized==0.9.0', 'behave==1.2.6', # Newer ones had issues 'soupsieve~=2.8.4;python_version<"3.10"', - 'soupsieve~=2.9.1;python_version>="3.10"', + 'soupsieve~=2.9.2;python_version>="3.10"', 'beautifulsoup4~=4.15.0', 'pyotp~=2.10.0', 'python-xlib==0.33;platform_system=="Linux"', @@ -255,7 +255,7 @@ # Usage: coverage run -m pytest; coverage html; coverage report "coverage": [ 'coverage>=7.10.7;python_version<"3.10"', - 'coverage>=7.15.3;python_version>="3.10"', + 'coverage>=7.15.4;python_version>="3.10"', 'pytest-cov>=7.1.0', ], # pip install -e .[flake8]