33require 'rbconfig'
44require 'openssl'
55require 'tmpdir'
6+ require 'fileutils'
67require 'browserstack/localexception'
8+ require 'browserstack/fetch_download_source_url'
9+ require 'browserstack/version'
710
811module BrowserStack
9-
12+
1013class LocalBinary
11- def initialize
12- host_os = RbConfig ::CONFIG [ 'host_os' ]
13- @http_path = case host_os
14- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
15- @windows = true
16- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal.exe"
17- when /darwin|mac os/
18- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-darwin-x64"
19- when /linux-musl/
20- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-alpine"
21- when /linux/
22- if 1 . size == 8
23- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-x64"
24- else
25- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-ia32"
26- end
27- end
14+ BASE_RETRIES = 9
15+ FALLBACK_TRIGGER_RETRY = 4
16+
17+ def initialize ( conf = { } )
18+ @auth_token = conf [ :auth_token ] || ENV [ 'BROWSERSTACK_ACCESS_KEY' ]
19+ @proxy_host = conf [ :proxy_host ]
20+ @proxy_port = conf [ :proxy_port ]
21+ @user_agent = conf [ :user_agent ] || "browserstack-local-ruby/#{ BrowserStack ::VERSION } "
22+
23+ @windows = !!( RbConfig ::CONFIG [ 'host_os' ] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/ )
24+ @binary_filename = compute_binary_filename
25+ @source_url = nil
26+ @download_error_message = nil
2827
2928 @ordered_paths = [
3029 File . join ( File . expand_path ( '~' ) , '.browserstack' ) ,
@@ -33,79 +32,125 @@ def initialize
3332 ]
3433 end
3534
36- def download ( dest_parent_dir )
37- unless File . exist? dest_parent_dir
38- Dir . mkdir dest_parent_dir
39- end
40- uri = URI . parse ( @http_path )
41- binary_path = File . join ( dest_parent_dir , "BrowserStackLocal#{ ".exe" if @windows } " )
42- http = Net ::HTTP . new ( uri . host , uri . port )
43- http . use_ssl = true
44- http . verify_mode = OpenSSL ::SSL ::VERIFY_NONE
45-
46- res = http . get ( uri . path )
47- file = open ( binary_path , 'wb' )
48- file . write ( res . body )
49- file . close
50- FileUtils . chmod 0755 , binary_path
51-
52- binary_path
53- end
35+ def binary_path
36+ dest_parent_dir = get_available_dirs
37+ bin_path = File . join ( dest_parent_dir , dest_binary_name )
5438
55- def verify_binary ( binary_path )
56- binary_response = IO . popen ( binary_path + " --version" ) . readline
57- binary_response =~ /BrowserStack Local version \d +\. \d +/
58- rescue Exception => e
59- false
39+ return bin_path if File . exist? ( bin_path ) && verify_binary ( bin_path )
40+
41+ File . delete ( bin_path ) if File . exist? ( bin_path )
42+ download_with_retries ( bin_path )
6043 end
6144
62- def binary_path
63- dest_parent_dir = get_available_dirs
64- binary_path = File . join ( dest_parent_dir , "BrowserStackLocal#{ ".exe" if @windows } " )
45+ private
6546
66- if File . exist? binary_path
67- binary_path
47+ def dest_binary_name
48+ @windows ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'
49+ end
50+
51+ def compute_binary_filename
52+ host_os = RbConfig ::CONFIG [ 'host_os' ]
53+ host_cpu = RbConfig ::CONFIG [ 'host_cpu' ]
54+ case host_os
55+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
56+ 'BrowserStackLocal.exe'
57+ when /darwin|mac os/
58+ 'BrowserStackLocal-darwin-x64'
59+ when /linux/
60+ if host_cpu =~ /arm64|aarch64/
61+ 'BrowserStackLocal-linux-arm64'
62+ elsif host_os =~ /linux-musl/
63+ 'BrowserStackLocal-alpine'
64+ elsif 1 . size == 8
65+ 'BrowserStackLocal-linux-x64'
66+ else
67+ 'BrowserStackLocal-linux-ia32'
68+ end
6869 else
69- binary_path = download ( dest_parent_dir )
70+ raise BrowserStack :: LocalException . new ( "Unsupported host OS: #{ host_os } " )
7071 end
72+ end
7173
72- valid_binary = verify_binary ( binary_path )
73-
74- if valid_binary
75- binary_path
76- else
77- binary_path = download ( dest_parent_dir )
78- valid_binary = verify_binary ( binary_path )
79- if valid_binary
80- binary_path
81- else
82- raise BrowserStack ::LocalException . new ( 'BrowserStack Local binary is corrupt' )
74+ def download_with_retries ( bin_path )
75+ retries = BASE_RETRIES
76+ while retries > 0
77+ refresh_source_url ( retries ) if retries == BASE_RETRIES || retries == FALLBACK_TRIGGER_RETRY
78+ begin
79+ download_to ( @source_url + '/' + @binary_filename , bin_path )
80+ return bin_path if verify_binary ( bin_path )
81+ @download_error_message = 'Downloaded binary failed verification'
82+ rescue StandardError => e
83+ @download_error_message = "Download failed: #{ e . message } "
8384 end
85+ File . delete ( bin_path ) if File . exist? ( bin_path )
86+ retries -= 1
8487 end
88+
89+ raise BrowserStack ::LocalException . new (
90+ "Failed to download BrowserStack Local binary after #{ BASE_RETRIES } attempts. " \
91+ "Last error: #{ @download_error_message } "
92+ )
8593 end
8694
87- private
95+ def refresh_source_url ( retries )
96+ is_fallback = ( retries == FALLBACK_TRIGGER_RETRY ) && !@download_error_message . nil?
97+ begin
98+ @source_url = BrowserStack ::FetchDownloadSourceUrl . call (
99+ auth_token : @auth_token ,
100+ user_agent : @user_agent ,
101+ fallback : is_fallback ,
102+ error_message : @download_error_message ,
103+ proxy_host : @proxy_host ,
104+ proxy_port : @proxy_port
105+ )
106+ rescue StandardError => e
107+ raise if @source_url . nil?
108+ @download_error_message = "Source URL refresh failed: #{ e . message } "
109+ end
110+ end
111+
112+ def download_to ( url , bin_path )
113+ uri = URI . parse ( url )
114+ http_class = if @proxy_host && @proxy_port
115+ Net ::HTTP ::Proxy ( @proxy_host , @proxy_port . to_i )
116+ else
117+ Net ::HTTP
118+ end
119+ http = http_class . new ( uri . host , uri . port )
120+ http . use_ssl = ( uri . scheme == 'https' )
121+ http . verify_mode = OpenSSL ::SSL ::VERIFY_PEER
122+ http . open_timeout = 10
123+ http . read_timeout = 30
124+
125+ req = Net ::HTTP ::Get . new ( uri . request_uri )
126+ req [ 'User-Agent' ] = @user_agent
127+
128+ res = http . request ( req )
129+ raise "HTTP #{ res . code } " unless res . is_a? ( Net ::HTTPSuccess )
130+
131+ File . open ( bin_path , 'wb' ) { |f | f . write ( res . body ) }
132+ FileUtils . chmod 0755 , bin_path
133+ end
134+
135+ def verify_binary ( bin_path )
136+ binary_response = IO . popen ( bin_path + " --version" ) . readline
137+ !!( binary_response =~ /BrowserStack Local version \d +\. \d +/ )
138+ rescue StandardError
139+ false
140+ end
88141
89142 def get_available_dirs
90- i = 0
91- while i < @ordered_paths . size
92- path = @ordered_paths [ i ]
93- if make_path ( path )
94- return path
95- else
96- i += 1
97- end
143+ @ordered_paths . each do |path |
144+ return path if make_path ( path )
98145 end
99146 raise BrowserStack ::LocalException . new ( 'Error trying to download BrowserStack Local binary' )
100147 end
101148
102149 def make_path ( path )
103- begin
104- FileUtils . mkdir_p path if !File . directory? ( path )
105- return true
106- rescue Exception
107- return false
108- end
150+ FileUtils . mkdir_p ( path ) unless File . directory? ( path )
151+ true
152+ rescue StandardError
153+ false
109154 end
110155end
111156
0 commit comments