Spaces:
No application file
No application file
| GOOGLE_CHROME = 'Google Chrome' | |
| MICROSOFT_EDGE = 'Microsoft Edge' | |
| MOZILLA_FIREFOX = 'Mozilla Firefox' | |
| APPLE_SAFARI = 'Apple Safari' | |
| GOOGLE_CHROME_RE = r'(\d+\.\d+\.\d+\.\d+)' | |
| MICROSOFT_EDGE_RE = r'(\d+\.\d+\.\d+\.\d+)' | |
| MOZILLA_FIREFOX_RE = r'(\d+\.\d+\.\d+)|(\d+\.\d+)' | |
| APPLE_SAFARI_RE = r'\d+.\d+.\d+' | |
| from .SharedTools import console_log, INFO, OK, ERROR, WARN | |
| from .ProgressBar import ProgressBar, DEFAULT_RICH_STYLE | |
| from pathlib import Path | |
| from colorama import Fore, init | |
| init() | |
| import subprocess | |
| import platform | |
| import requests | |
| import logging | |
| import zipfile | |
| import tarfile | |
| import shutil | |
| import sys | |
| import re | |
| import os | |
| SILENT_MODE = '--silent' in sys.argv | |
| class WebDriverInstaller(object): | |
| def __init__(self, browser_name: str, custom_browser_location=None): | |
| self.browsers_data = { | |
| GOOGLE_CHROME: [self.get_chromedriver_url, 'chromedriver.exe' if sys.platform.startswith('win') else 'chromedriver', self.get_chrome_version, GOOGLE_CHROME_RE], | |
| MICROSOFT_EDGE: [self.get_msedgedriver_url, 'msedgedriver.exe' if sys.platform.startswith('win') else 'msedgedriver', self.get_edge_version, MICROSOFT_EDGE_RE], | |
| MOZILLA_FIREFOX: [self.get_geckodriver_url, 'geckodriver.exe' if sys.platform.startswith('win') else 'geckodriver', self.get_firefox_version, MOZILLA_FIREFOX_RE], | |
| APPLE_SAFARI: [] | |
| } | |
| self.browser_name = browser_name | |
| self.custom_browser_location = custom_browser_location | |
| if self.browser_name not in self.browsers_data: | |
| raise RuntimeError('WebDriverInstaller: invalid browser_name!') | |
| self.browser_data = self.browsers_data[self.browser_name] | |
| self.platform = ['', []] # [OC name, [webdriver architectures]] | |
| if sys.platform.startswith('win'): | |
| self.platform[0] = 'win' | |
| if sys.maxsize > 2**32: | |
| self.platform[1] = ['win64', 'win32'] | |
| else: | |
| self.platform[1] = ['win32'] | |
| elif sys.platform.startswith('linux'): | |
| self.platform[0] = 'linux' | |
| if sys.maxsize > 2**32: | |
| self.platform[1].append('linux64') | |
| else: | |
| self.platform[1].append('linux32') | |
| elif sys.platform == "darwin": | |
| self.platform[0] = 'mac' | |
| if self.browser_name == MOZILLA_FIREFOX: | |
| self.platform[1] = ['macos'] | |
| elif platform.processor() == "arm": | |
| self.platform[1] = ['mac-arm64', 'mac_arm64', 'mac64_m1'] | |
| if self.browser_name == MOZILLA_FIREFOX: | |
| self.platform[1] = ['macos-aarch64'] | |
| elif platform.processor() == "i386": | |
| self.platform[1] = ['mac64', 'mac-x64'] | |
| def get_browser_version_from_cmd(self, path: str, re_pattern: str): | |
| try: | |
| with subprocess.Popen([path, "--version"], stdout=subprocess.PIPE) as proc: | |
| return re.search(re_pattern, proc.communicate()[0].decode("utf-8")).group() | |
| except: | |
| pass | |
| def get_chrome_version(self): | |
| browser_version = None | |
| browser_path = None | |
| if self.platform[0] == "linux": | |
| if self.custom_browser_location is not None: | |
| browser_version = self.get_browser_version_from_cmd(self.custom_browser_location, GOOGLE_CHROME_RE) | |
| browser_path = self.custom_browser_location | |
| else: | |
| for executable in ["google-chrome", "google-chrome-stable", "google-chrome-beta", "google-chrome-dev", "chromium-browser", "chromium"]: | |
| browser_version = self.get_browser_version_from_cmd(shutil.which(executable), GOOGLE_CHROME_RE) | |
| if browser_version is not None: | |
| browser_path = shutil.which(executable) | |
| break | |
| elif self.platform[0] == "mac": | |
| if self.custom_browser_location is not None: | |
| browser_version = self.get_browser_version_from_cmd(self.custom_browser_location, GOOGLE_CHROME_RE) | |
| browser_path = self.custom_browser_location | |
| else: | |
| browser_version = self.get_browser_version_from_cmd('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', GOOGLE_CHROME_RE) | |
| browser_path = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' | |
| elif self.platform[0] == "win": | |
| paths = [ | |
| f'{os.environ.get("SYSTEMDRIVE")}\\Program Files\\Google\\Chrome\\Application', | |
| f'{os.environ.get("SYSTEMDRIVE")}\\Program Files (x86)\\Google\\Chrome\\Application', | |
| f'{os.environ.get("LOCALAPPDATA")}\\Google\\Chrome\\Application' | |
| ] | |
| if self.custom_browser_location is not None: | |
| paths = [str(Path(self.custom_browser_location).parent)] | |
| for path in paths: | |
| try: | |
| with open(path+'\\chrome.VisualElementsManifest.xml', 'r') as f: | |
| browser_version = re.search(GOOGLE_CHROME_RE, f.read()).group() | |
| browser_path = path+'\\chrome.exe' | |
| break | |
| except: | |
| pass | |
| return [browser_version, browser_path] | |
| def get_chromedriver_url(self, chrome_major_version=None): | |
| if chrome_major_version is None: | |
| chrome_major_version = self.get_chrome_version()[0] | |
| if chrome_major_version is None: | |
| return None | |
| chrome_major_version = self.get_chrome_version()[0].split('.')[0] | |
| if int(chrome_major_version) >= 115: # for new drivers ( [115.0.0000.0, ...] ) | |
| drivers_data = requests.get('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json') | |
| drivers_data = drivers_data.json()['versions'][::-1] # start with the latest version | |
| for driver_data in drivers_data: | |
| driver_major_version = driver_data['version'].split('.')[0] # major, _, minor, micro | |
| if driver_major_version == chrome_major_version: # return latest driver version for current major chrome version | |
| for driver_url in driver_data['downloads'].get('chromedriver', []): | |
| if driver_url['platform'] in self.platform[1]: | |
| return driver_url['url'] | |
| else: # for old drivers ( [..., 115.0.0000.0) ) | |
| latest_old_driver_version = requests.get(f'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{chrome_major_version}') | |
| if latest_old_driver_version.status_code == 200: | |
| latest_old_driver_version = latest_old_driver_version.text | |
| driver_url = f'https://chromedriver.storage.googleapis.com/{latest_old_driver_version}/chromedriver_' | |
| for arch in self.platform[1]: | |
| current_driver_url = driver_url+arch+'.zip' | |
| driver_size = requests.head(current_driver_url).headers.get('x-goog-stored-content-length', None) | |
| if driver_size is not None and int(driver_size) > 1024**2: | |
| return current_driver_url | |
| def get_edge_version(self): | |
| browser_version = None | |
| browser_path = None | |
| if self.platform[0] == 'linux': | |
| if self.custom_browser_location is not None: | |
| browser_version = self.get_browser_version_from_cmd(self.custom_browser_location, MICROSOFT_EDGE_RE) | |
| browser_path = self.custom_browser_location | |
| else: | |
| for executable in ['microsoft-edge']: | |
| browser_version = self.get_browser_version_from_cmd(shutil.which(executable), MICROSOFT_EDGE_RE) | |
| if browser_version is not None: | |
| browser_path = shutil.which(executable) | |
| break | |
| elif self.platform[0] == "mac": | |
| if self.custom_browser_location is not None: | |
| browser_version = self.get_browser_version_from_cmd(self.custom_browser_location, MICROSOFT_EDGE_RE) | |
| browser_path = self.custom_browser_location | |
| else: | |
| browser_version = self.get_browser_version_from_cmd('/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', MICROSOFT_EDGE_RE) | |
| browser_path = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge' | |
| elif self.platform[0] == 'win': | |
| paths = [ | |
| f'{os.environ.get("SYSTEMDRIVE")}\\Program Files\\Microsoft\\Edge\\Application', | |
| f'{os.environ.get("SYSTEMDRIVE")}\\Program Files (x86)\\Microsoft\\Edge\\Application' | |
| ] | |
| if self.custom_browser_location is not None: | |
| paths = [str(Path(self.custom_browser_location).parent)] | |
| for path in paths: | |
| try: | |
| with open(path+'\\msedge.VisualElementsManifest.xml', 'r') as f: | |
| browser_version = re.search(MICROSOFT_EDGE_RE, f.read()).group() | |
| browser_path = path+'\\msedge.exe' | |
| break | |
| except: | |
| pass | |
| return [browser_version, browser_path] | |
| def get_msedgedriver_url(self, edge_version=None): | |
| archs = self.platform[1] | |
| if edge_version is None: | |
| edge_version, _ = self.get_edge_version() | |
| major_version = edge_version.split('.')[0] | |
| if self.platform[0] == 'win': | |
| r = requests.get(f'https://msedgedriver.azureedge.net/LATEST_RELEASE_{major_version}_WINDOWS') | |
| elif self.platform[0] == 'linux': | |
| r = requests.get(f'https://msedgedriver.azureedge.net/LATEST_RELEASE_{major_version}_LINUX') | |
| elif self.platform[0] == 'mac': | |
| r = requests.get(f'https://msedgedriver.azureedge.net/LATEST_RELEASE_{major_version}_MACOS') | |
| if r.status_code == 200: | |
| webdriver_version = r.text.strip() | |
| for arch in archs: | |
| driver_url = f'https://msedgedriver.azureedge.net/{webdriver_version}/edgedriver_{arch}.zip' | |
| driver_size = requests.head(driver_url).headers.get('Content-Length', None) | |
| if driver_size is not None and int(driver_size) > 1024**2: | |
| return driver_url | |
| def get_firefox_version(self): | |
| browser_version = None | |
| browser_path = None | |
| if self.platform[0] == 'linux': | |
| if self.custom_browser_location is not None: | |
| browser_version = self.get_browser_version_from_cmd(self.custom_browser_location, MOZILLA_FIREFOX_RE) | |
| browser_path = self.custom_browser_location | |
| else: | |
| for executable in ['firefox']: | |
| browser_version = self.get_browser_version_from_cmd(shutil.which(executable), MOZILLA_FIREFOX_RE) | |
| if browser_version is not None: | |
| browser_path = shutil.which(executable) | |
| break | |
| elif self.platform[0] == "mac": | |
| if self.custom_browser_location is not None: | |
| browser_version = self.get_browser_version_from_cmd(self.custom_browser_location, MOZILLA_FIREFOX_RE) | |
| browser_path = self.custom_browser_location | |
| else: | |
| for path in ['/Applications/Firefox.app/Contents/MacOS/firefox', '/application/firefox.app']: | |
| if browser_version is not None: | |
| browser_version = self.get_browser_version_from_cmd(path, MOZILLA_FIREFOX_RE) | |
| browser_path = path | |
| break | |
| elif self.platform[0] == 'win': | |
| paths = [ | |
| f'{os.environ.get("SYSTEMDRIVE")}\\Program Files\\Mozilla Firefox', | |
| f'{os.environ.get("SYSTEMDRIVE")}\\Program Files (x86)\\Mozilla Firefox', | |
| ] | |
| if self.custom_browser_location is not None: | |
| paths = [str(Path(self.custom_browser_location).parent)] | |
| for path in paths: | |
| try: | |
| with open(path+'\\application.ini', 'r') as f: | |
| browser_version = re.search(MOZILLA_FIREFOX_RE, f.read()).group() | |
| browser_path = path+'\\firefox.exe' | |
| break | |
| except: | |
| pass | |
| return [browser_version, browser_path] | |
| def get_geckodriver_url(self, only_version=False): | |
| r = requests.get("https://api.github.com/repos/mozilla/geckodriver/releases/latest") | |
| r_json = r.json() | |
| api_rate_limit = (True if r_json.get('name', None) is None else False) | |
| if api_rate_limit: # bypass for API rate limit exceeded for your IP | |
| r = requests.head("https://github.com/mozilla/geckodriver/releases/latest", allow_redirects=True) | |
| geckodriver_version = r.url.split('/')[-1][1:] | |
| else: | |
| geckodriver_version = r_json['name'] | |
| if only_version: | |
| return geckodriver_version | |
| if not api_rate_limit: | |
| #https://github.com/mozilla/geckodriver/releases/download/v0.34.0/geckodriver-v0.34.0-macos.tar.gz | |
| # note for: r_json['assets'][::-1] | |
| # in the initialization of WebDriverInstaller for 64bit is also suitable for 32bit, but | |
| # in the list of assets first go 32bit and it comes out that for 64bit gives a 32bit release, turning the list fixes it | |
| for asset in r_json['assets'][::-1]: | |
| if asset['name'].find('asc') == -1: # ignoring GPG Keys | |
| asset_arch = asset['name'].split('-', 2)[-1].split('.')[0] # package architecture parsing; geckodriver-v0.34.0-win32.zip -> ['geckodriver', 'v0.34.0', 'win32.zip'] -> ['win32', 'zip'] -> win32 | |
| if asset_arch in self.platform[1]: | |
| return asset['browser_download_url'] | |
| else: | |
| # bypass for API rate limit exceeded for your IP | |
| extension = '.zip' if self.platform[0] == 'win' else '.tar.gz' | |
| for arch in self.platform[1]: | |
| url = f'https://github.com/mozilla/geckodriver/releases/download/v{geckodriver_version}/geckodriver-v{geckodriver_version}-{arch}{extension}' | |
| r = requests.get(url, stream=True) | |
| if int(r.headers.get('Content-Length', 0)) > 1024**2: | |
| return url | |
| def get_safari_version(self): | |
| if self.platform[0] == "mac": | |
| cmd = ['/usr/libexec/PlistBuddy', '-c', "print :CFBundleShortVersionString", '/Applications/Safari.app/Contents/Info.plist'] | |
| try: | |
| with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc: | |
| return re.search(APPLE_SAFARI_RE, proc.communicate()[0].decode("utf-8")).group() | |
| except: | |
| pass | |
| def detect_installed_browser(self): | |
| for browser_name in self.browsers_data: | |
| if browser_name == APPLE_SAFARI: | |
| browser_version = self.get_safari_version() | |
| if browser_version is not None: | |
| return [APPLE_SAFARI, browser_version] | |
| else: | |
| browser_version, browser_path = self.browsers_data[browser_name][2]() | |
| if browser_version is not None: | |
| return [browser_name, browser_version, browser_path] | |
| def download_webdriver(self, url=None, path='.', disable_progress_bar=False): | |
| # init | |
| webdriver_name = self.browser_data[1] | |
| file_extension = '.zip' | |
| if url is None: | |
| url = self.browser_data[0]() | |
| if url is None: | |
| return None | |
| if url.split('.')[-1] == 'gz': | |
| file_extension = '.tar.gz' | |
| # downloading | |
| archive_path = str(Path(f'{path}/data{file_extension}').resolve()) | |
| response = requests.get(url, stream=True) | |
| if not disable_progress_bar: | |
| total_length = int(response.headers.get('Content-Length', 0)) | |
| total_length = int(response.headers.get('content-length', total_length)) | |
| else: | |
| total_length = 0 | |
| if total_length == 0 or SILENT_MODE: # No content length header | |
| with open(archive_path, 'wb') as f: | |
| f.write(response.content) | |
| else: | |
| task = ProgressBar(int(total_length), ' ', DEFAULT_RICH_STYLE) | |
| with open(archive_path, 'wb') as f: | |
| for chunk in response.iter_content(chunk_size=8192): | |
| if chunk: # filter out keep-alive new chunks | |
| f.write(chunk) | |
| task.update(len(chunk)) | |
| task.render() | |
| # extracting | |
| archive, info = None, [] | |
| if file_extension == '.zip': | |
| archive = zipfile.ZipFile(archive_path) | |
| archive_info = archive.infolist() | |
| elif file_extension == '.tar.gz': | |
| archive = tarfile.open(archive_path) | |
| archive_info = archive.getnames() | |
| if archive is not None: | |
| for info in archive_info: | |
| archive_filename, archive_filepath = None, None | |
| if file_extension == '.zip': | |
| archive_filename, archive_filepath = info.filename.split('/')[-1], info.filename | |
| else: | |
| archive_filename, archive_filepath = info.split('/')[-1], info | |
| if archive_filename is not None and archive_filename == webdriver_name: | |
| try: | |
| archive.extract(info) | |
| archive.close() | |
| webdriver_path = str(Path(archive_filepath).resolve()) | |
| if Path(archive_filepath).resolve().parent != Path(os.getcwd()): | |
| webdriver_path = shutil.copy2(str(Path(archive_filepath).resolve()), os.getcwd()) | |
| try: | |
| shutil.rmtree(archive_filepath.split('/')[0], ignore_errors=True) | |
| os.remove(archive_path) | |
| except: | |
| pass | |
| os.chmod(webdriver_path, 0o777) | |
| return webdriver_path | |
| except: | |
| return None | |
| def menu(self, disable_progress_bar=False): # auto updating or installing webdrivers | |
| def download(): | |
| driver_url = self.browser_data[0]() | |
| if driver_url is not None: | |
| logging.info('Found a suitable version for your system!') | |
| logging.info('Downloading...') | |
| console_log('\nFound a suitable version for your system!', OK, silent_mode=SILENT_MODE) | |
| console_log('Downloading...', INFO, silent_mode=SILENT_MODE) | |
| if self.download_webdriver(driver_url, disable_progress_bar=disable_progress_bar): | |
| logging.info(f'{self.browser_name} webdriver was successfully downloaded and unzipped!') | |
| console_log(f'{self.browser_name} webdriver was successfully downloaded and unzipped!\n', OK, silent_mode=SILENT_MODE) | |
| return os.path.join(os.getcwd(), webdriver_name) | |
| else: | |
| logging.info('Error downloading or unpacking!') | |
| console_log('Error downloading or unpacking!\n', ERROR, silent_mode=SILENT_MODE) | |
| else: | |
| logging.info('A suitable version for your system was not found!') | |
| console_log('\nA suitable version for your system was not found!\n', ERROR, silent_mode=SILENT_MODE) | |
| logging.info('-- WebDriver Auto-Installer --') | |
| console_log(f'{Fore.LIGHTMAGENTA_EX}-- WebDriver Auto-Installer --{Fore.RESET}\n', silent_mode=SILENT_MODE) | |
| browser_version, browser_path = self.browser_data[2]() | |
| if browser_version is None: | |
| if self.custom_browser_location is None or self.custom_browser_location == '': | |
| raise RuntimeError(f'{self.browser_name} was not found in the standard catalogs!') | |
| raise RuntimeError(f'{self.custom_browser_location} is not a valid executable file of {self.browser_name}!') | |
| webdriver_name = self.browser_data[1] | |
| current_webdriver_version = None | |
| webdriver_path = None | |
| if os.path.exists(webdriver_name): | |
| try: | |
| out = subprocess.check_output([os.path.join(os.getcwd(), webdriver_name), "--version"], stderr=subprocess.PIPE) | |
| out = re.search(self.browser_data[3], out.decode('utf-8')) | |
| if out is not None: | |
| current_webdriver_version = out.group() | |
| webdriver_path = os.path.join(os.getcwd(), webdriver_name) | |
| except: | |
| pass | |
| logging.info(f'{self.browser_name} version: {browser_version}') | |
| logging.info(f'{self.browser_name} webdriver version: {current_webdriver_version}') | |
| console_log(f'{self.browser_name} version: {browser_version}', INFO, False, SILENT_MODE) | |
| console_log(f'{self.browser_name} webdriver version: {current_webdriver_version}', INFO, False, SILENT_MODE) | |
| if self.browser_name == MOZILLA_FIREFOX: | |
| latest_geckodriver_version = self.browser_data[0](True) | |
| if current_webdriver_version == latest_geckodriver_version: | |
| logging.info('The webdriver has already been updated to the latest version!') | |
| console_log('The webdriver has already been updated to the latest version!\n', OK, silent_mode=SILENT_MODE) | |
| webdriver_path = os.path.join(os.getcwd(), webdriver_name) | |
| else: | |
| logging.info(f'Updating the webdriver from {current_webdriver_version} to {latest_geckodriver_version} version...') | |
| console_log(f'Updating the webdriver from {current_webdriver_version} to {latest_geckodriver_version} version...', INFO, silent_mode=SILENT_MODE) | |
| webdriver_path = download() | |
| else: | |
| if current_webdriver_version is None or (current_webdriver_version.split('.')[0] != browser_version.split('.')[0]): # major version match | |
| logging.warning(f'{self.browser_name} webdriver version doesn\'t match version of the installed {self.browser_name}, trying to download...') | |
| console_log(f'{self.browser_name} webdriver version doesn\'t match version of the installed {self.browser_name}, trying to download...', WARN, True, SILENT_MODE) | |
| webdriver_path = download() | |
| else: | |
| logging.info('The webdriver has already been updated to the browser version!') | |
| console_log('The webdriver has already been updated to the browser version!\n', OK, silent_mode=SILENT_MODE) | |
| try: | |
| os.chmod(webdriver_path, 0o755) | |
| except: | |
| pass | |
| return [str(Path(webdriver_path).resolve()), str(Path(browser_path).resolve())] |