diff --git a/browserstack/local.py b/browserstack/local.py index d7e1618..544c791 100644 --- a/browserstack/local.py +++ b/browserstack/local.py @@ -15,6 +15,7 @@ def __init__(self, key=None, binary_path=None, **kwargs): self.key = os.environ['BROWSERSTACK_ACCESS_KEY'] if 'BROWSERSTACK_ACCESS_KEY' in os.environ else key self.options = kwargs self.local_logfile_path = os.path.join(os.getcwd(), 'local.log') + LocalBinary.set_version(self.get_package_version()) def __xstr(self, key, value): if key is None: @@ -24,7 +25,7 @@ def __xstr(self, key, value): elif str(value).lower() == "false": return [''] else: - return ['-' + key, value] + return ['-' + key, str(value)] def get_package_version(self): name = "browserstack-local" @@ -72,7 +73,12 @@ def start(self, **kwargs): self.binary_path = self.options['binarypath'] del self.options['binarypath'] else: - self.binary_path = LocalBinary().get_binary() + l = LocalBinary(self.key) + try: + self.binary_path = l.get_binary() + except Exception as e: + l = LocalBinary(self.key, e) + self.binary_path = l.get_binary() if 'logfile' in self.options: self.local_logfile_path = self.options['logfile'] diff --git a/browserstack/local_binary.py b/browserstack/local_binary.py index 4bec7e7..13144fa 100644 --- a/browserstack/local_binary.py +++ b/browserstack/local_binary.py @@ -1,29 +1,39 @@ import platform, os, sys, stat, tempfile, re, subprocess from browserstack.bserrors import BrowserStackLocalError +import gzip +import json try: - from urllib.request import urlopen + from urllib.request import urlopen, Request except ImportError: - from urllib2 import urlopen + from urllib2 import urlopen, Request class LocalBinary: - def __init__(self): + _version = None + + def __init__(self, key, error_object=None): + self.key = key + self.error_object = error_object is_64bits = sys.maxsize > 2**32 self.is_windows = False osname = platform.system() + source_url = self.fetch_source_url() + '/' + if osname == 'Darwin': - self.http_path = "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-darwin-x64" + self.http_path = source_url + "BrowserStackLocal-darwin-x64" elif osname == 'Linux': if self.is_alpine(): - self.http_path = "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-alpine" + self.http_path = source_url + "BrowserStackLocal-alpine" + elif platform.machine() == 'aarch64': + self.http_path = source_url + "BrowserStackLocal-linux-arm64" else: if is_64bits: - self.http_path = "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-x64" + self.http_path = source_url + "BrowserStackLocal-linux-x64" else: - self.http_path = "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-ia32" + self.http_path = source_url + "BrowserStackLocal-linux-ia32" else: self.is_windows = True - self.http_path = "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal.exe" + self.http_path = source_url + "BrowserStackLocal.exe" self.ordered_paths = [ os.path.join(os.path.expanduser('~'), '.browserstack'), @@ -32,11 +42,39 @@ def __init__(self): ] self.path_index = 0 + def fetch_source_url(self): + url = "https://local.browserstack.com/binary/api/v1/endpoint" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": '/'.join(('browserstack-local-python', LocalBinary._version)) + } + data = {"auth_token": self.key} + + if self.error_object is not None: + data["error_message"] = str(self.error_object) + headers["X-Local-Fallback-Cloudflare"] = "true" + + req = Request(url, data=json.dumps(data).encode("utf-8")) + for key, value in headers.items(): + req.add_header(key, value) + + try: + with urlopen(req) as response: + resp_bytes = response.read() + resp_str = resp_bytes.decode('utf-8') + resp_json = json.loads(resp_str) + return resp_json["data"]["endpoint"] + except Exception as e: + raise BrowserStackLocalError('Error trying to fetch the source url for downloading the binary: {}'.format(e)) + + @staticmethod + def set_version(version): + LocalBinary._version = version + def is_alpine(self): - grepOutput = subprocess.run("gfrep -w NAME /etc/os-release", capture_output=True, shell=True) - if grepOutput.stdout.decode('utf-8').find('Alpine') > -1: - return True - return False + response = subprocess.check_output(["grep", "-w", "NAME", "/etc/os-release"]) + return response.decode('utf-8').find('Alpine') > -1 def __make_path(self, dest_path): try: @@ -54,36 +92,64 @@ def __available_dir(self): return final_path else: self.path_index += 1 - raise BrowserStackLocalError('Error trying to download BrowserStack Local binary') + raise BrowserStackLocalError('Error trying to download BrowserStack Local binary, exhausted user directories to download to.') def download(self, chunk_size=8192, progress_hook=None): - response = urlopen(self.http_path) + headers = { + 'User-Agent': '/'.join(('browserstack-local-python', LocalBinary._version)), + 'Accept-Encoding': 'gzip, *', + } + + if sys.version_info < (3, 2): + # lack of support for gzip decoding for stream, response is expected to have a tell() method + headers.pop('Accept-Encoding', None) + + response = urlopen(Request(self.http_path, headers=headers)) try: - total_size = int(response.info().getheader('Content-Length').strip()) + total_size = int(response.info().get('Content-Length', '').strip() or '0') except: - total_size = int(response.info().get_all('Content-Length')[0].strip()) + total_size = int(response.info().get_all('Content-Length')[0].strip() or '0') bytes_so_far = 0 + # Limits retries to the number of directories dest_parent_dir = self.__available_dir() dest_binary_name = 'BrowserStackLocal' if self.is_windows: dest_binary_name += '.exe' + content_encoding = response.info().get('Content-Encoding', '') + gzip_file = gzip.GzipFile(fileobj=response, mode='rb') if content_encoding.lower() == 'gzip' else None + + if os.getenv('BROWSERSTACK_LOCAL_DEBUG_GZIP') and gzip_file: + print('using gzip in ' + headers['User-Agent']) + + def read_chunk(chunk_size): + if gzip_file: + return gzip_file.read(chunk_size) + else: + return response.read(chunk_size) + with open(os.path.join(dest_parent_dir, dest_binary_name), 'wb') as local_file: while True: - chunk = response.read(chunk_size) + chunk = read_chunk(chunk_size) bytes_so_far += len(chunk) if not chunk: break - if progress_hook: + if total_size > 0 and progress_hook: progress_hook(bytes_so_far, chunk_size, total_size) try: local_file.write(chunk) except: return self.download(chunk_size, progress_hook) + + if gzip_file: + gzip_file.close() + + if callable(getattr(response, 'close', None)): + response.close() final_path = os.path.join(dest_parent_dir, dest_binary_name) st = os.stat(final_path) @@ -102,7 +168,8 @@ def get_binary(self): dest_parent_dir = os.path.join(os.path.expanduser('~'), '.browserstack') if not os.path.exists(dest_parent_dir): os.makedirs(dest_parent_dir) - bsfiles = [f for f in os.listdir(dest_parent_dir) if f.startswith('BrowserStackLocal')] + binary_name = 'BrowserStackLocal.exe' if self.is_windows else 'BrowserStackLocal' + bsfiles = [f for f in os.listdir(dest_parent_dir) if f == binary_name] if len(bsfiles) == 0: binary_path = self.download() diff --git a/requirements-release.txt b/requirements-release.txt new file mode 100644 index 0000000..6673e4d --- /dev/null +++ b/requirements-release.txt @@ -0,0 +1,3 @@ +build==1.4.0 +pkginfo==1.12.1.2 +twine==6.2.0 diff --git a/setup.cfg b/setup.cfg index 8c28267..7d0748b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [metadata] -description-file = README.md +description_file = README.md diff --git a/setup.py b/setup.py index c6a76fc..7cf8cc6 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,10 @@ setup( name = 'browserstack-local', packages = ['browserstack'], - version = '1.2.6', + version = '1.2.14', description = 'Python bindings for Browserstack Local', + long_description=open('README.md').read(), + long_description_content_type='text/markdown', author = 'BrowserStack', author_email = 'support@browserstack.com', url = 'https://github.com/browserstack/browserstack-local-python', diff --git a/tests/test_local.py b/tests/test_local.py index 11d6a02..d5d4e46 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -70,7 +70,7 @@ def test_proxy(self): self.assertIn('-proxyHost', self.local._generate_cmd()) self.assertIn('localhost', self.local._generate_cmd()) self.assertIn('-proxyPort', self.local._generate_cmd()) - self.assertIn(2000, self.local._generate_cmd()) + self.assertIn('2000', self.local._generate_cmd()) self.assertIn('-proxyUser', self.local._generate_cmd()) self.assertIn('hello', self.local._generate_cmd()) self.assertIn('-proxyPass', self.local._generate_cmd())