File size: 23,460 Bytes
308cf3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
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())]