diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index 6256b8128f8..273e6441540 100644 --- a/help_docs/cdp_mode_methods.md +++ b/help_docs/cdp_mode_methods.md @@ -12,6 +12,7 @@ sb.cdp.open(url, **kwargs) # Same as sb.cdp.get(url, **kwargs) sb.cdp.reload(ignore_cache=True, script_to_evaluate_on_load=None) sb.cdp.refresh(*args, **kwargs) sb.cdp.get_event_loop() +sb.cdp.is_running() sb.cdp.get_rd_host() # Returns the remote-debugging host sb.cdp.get_rd_port() # Returns the remote-debugging port sb.cdp.get_rd_url() # Returns the remote-debugging URL diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 0f6b71c814d..d026cb8566a 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -3,7 +3,7 @@ regex>=2026.5.9 pymdown-extensions>=10.21.3 -pipdeptree>=2.35.2 +pipdeptree>=2.35.3 python-dateutil>=2.8.2 Markdown==3.10.2 click==8.4.0 diff --git a/requirements.txt b/requirements.txt index b32e8c85fcc..b31aab1a32d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ six>=1.17.0 parse>=1.22.0 parse-type>=0.6.6 colorama>=0.4.6 +psutil>=7.2.2 pyyaml>=6.0.3 pygments>=2.20.0 pyreadline3>=3.5.4;platform_system=="Windows" diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 1a42e8d286d..4c0c2d16a1f 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.49.0" +__version__ = "4.49.1" diff --git a/seleniumbase/undetected/__init__.py b/seleniumbase/undetected/__init__.py index 3b316a21671..71cce497ba8 100644 --- a/seleniumbase/undetected/__init__.py +++ b/seleniumbase/undetected/__init__.py @@ -1,6 +1,7 @@ import logging import os import re +import psutil import requests import subprocess import sys @@ -299,6 +300,13 @@ def __init__( creationflags=creationflags, ) self.browser_pid = browser.pid + self._process_pid = browser.pid + try: + self._process_create_time = ( + psutil.Process(self._process_pid).create_time() + ) + except psutil.NoSuchProcess: + self._process_create_time = None service_ = None log_output = subprocess.PIPE if patch_driver: @@ -392,6 +400,22 @@ def _hook_remove_cdc_props(self, cdc_props): }, ) + def is_running(self): + """Check if the browser is still running.""" + if not getattr(self, "_process_pid", None): + return False + # Not good enough: return psutil.pid_exists(self._process_pid) + try: + process = psutil.Process(self._process_pid) + # Check for PID recycling: Is this actually our process? + if hasattr(self, "_process_create_time"): + if process.create_time() != self._process_create_time: + return False # The PID was reused by a different process! + # If the process is a zombie, assume the browser isn't running + return process.status() != psutil.STATUS_ZOMBIE + except psutil.NoSuchProcess: + return False + def remove_cdc_props_as_needed(self): cdc_props = self._get_cdc_props() if len(cdc_props) > 0: diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 6d28096b132..2f25edb65be 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -9,6 +9,7 @@ import os import pathlib import pickle +import psutil import re import shutil import time @@ -263,6 +264,22 @@ async def _handle_target_update( ) self.targets.remove(current_tab) + def is_running(self): + """Check if the browser is still running.""" + if not getattr(self, "_process_pid", None): + return False + # Not good enough: return psutil.pid_exists(self._process_pid) + try: + process = psutil.Process(self._process_pid) + # Check for PID recycling: Is this actually our process? + if hasattr(self, "_process_create_time"): + if process.create_time() != self._process_create_time: + return False # The PID was reused by a different process! + # If the process is a zombie, assume the browser isn't running + return process.status() != psutil.STATUS_ZOMBIE + except psutil.NoSuchProcess: + return False + def get_rd_host(self): return self.config.host @@ -617,6 +634,12 @@ async def start(self=None) -> Browser: ) await asyncio.sleep(0.05) self._process_pid = self._process.pid + try: + self._process_create_time = ( + psutil.Process(self._process_pid).create_time() + ) + except psutil.NoSuchProcess: + self._process_create_time = None await asyncio.sleep(0.05) self._http = HTTPApi((self.config.host, self.config.port)) await asyncio.sleep(0.05) diff --git a/setup.py b/setup.py index 4cdae98aa6a..f725f42615a 100755 --- a/setup.py +++ b/setup.py @@ -182,11 +182,12 @@ 'typing-extensions>=4.15.0', 'sbvirtualdisplay>=1.4.0', 'MarkupSafe>=3.0.3', - "Jinja2>=3.1.6", - "six>=1.17.0", + 'Jinja2>=3.1.6', + 'six>=1.17.0', 'parse>=1.22.0', 'parse-type>=0.6.6', 'colorama>=0.4.6', + 'psutil>=7.2.2', 'pyyaml>=6.0.3', 'pygments>=2.20.0', 'pyreadline3>=3.5.4;platform_system=="Windows"', @@ -303,11 +304,7 @@ # pip install -e .[playwright] # (For the Playwright integration.) "playwright": [ - "playwright>=1.58.0", - ], - # pip install -e .[psutil] - "psutil": [ - "psutil>=7.2.2", + "playwright>=1.60.0", ], # pip install -e .[pyautogui] # (Already a required dependency on Linux now.)