diff --git a/BrowserStackLocal/BrowserStackLocal Unit Tests/BrowserStackTunnelTests.cs b/BrowserStackLocal/BrowserStackLocal Unit Tests/BrowserStackTunnelTests.cs index 2a32216..ad13e95 100644 --- a/BrowserStackLocal/BrowserStackLocal Unit Tests/BrowserStackTunnelTests.cs +++ b/BrowserStackLocal/BrowserStackLocal Unit Tests/BrowserStackTunnelTests.cs @@ -30,14 +30,14 @@ public void TestInitialState() public void TestBinaryPathIsSet() { tunnel = new TunnelClass(); - tunnel.addBinaryPath("dummyPath"); + tunnel.addBinaryPath("dummyPath", ""); Assert.AreEqual(tunnel.getBinaryAbsolute(), "dummyPath"); } [TestMethod] public void TestBinaryPathOnNull() { tunnel = new TunnelClass(); - tunnel.addBinaryPath(null); + tunnel.addBinaryPath(null, ""); string expectedPath = Path.Combine(homepath, ".browserstack"); expectedPath = Path.Combine(expectedPath, binaryName); Assert.AreEqual(tunnel.getBinaryAbsolute(), expectedPath); @@ -46,7 +46,7 @@ public void TestBinaryPathOnNull() public void TestBinaryPathOnEmpty() { tunnel = new TunnelClass(); - tunnel.addBinaryPath(""); + tunnel.addBinaryPath("", ""); string expectedPath = Path.Combine(homepath, ".browserstack"); expectedPath = Path.Combine(expectedPath, binaryName); Assert.AreEqual(tunnel.getBinaryAbsolute(), expectedPath); @@ -56,7 +56,7 @@ public void TestBinaryPathOnFallback() { string expectedPath = "dummyPath"; tunnel = new TunnelClass(); - tunnel.addBinaryPath("dummyPath"); + tunnel.addBinaryPath("dummyPath", ""); Assert.AreEqual(tunnel.getBinaryAbsolute(), expectedPath); tunnel.fallbackPaths(); @@ -78,7 +78,7 @@ public void TestBinaryPathOnFallback() public void TestBinaryPathOnNoMoreFallback() { tunnel = new TunnelClass(); - tunnel.addBinaryPath("dummyPath"); + tunnel.addBinaryPath("dummyPath", ""); tunnel.fallbackPaths(); tunnel.fallbackPaths(); tunnel.fallbackPaths(); diff --git a/BrowserStackLocal/BrowserStackLocal Unit Tests/LocalTests.cs b/BrowserStackLocal/BrowserStackLocal Unit Tests/LocalTests.cs index ff03d1e..8be3eb6 100644 --- a/BrowserStackLocal/BrowserStackLocal Unit Tests/LocalTests.cs +++ b/BrowserStackLocal/BrowserStackLocal Unit Tests/LocalTests.cs @@ -107,7 +107,7 @@ public void TestWorksForBinaryPath() tunnelMock.Setup(mock => mock.Run("dummyKey", "", logAbsolute, "start")); local.setTunnel(tunnelMock.Object); local.start(options); - tunnelMock.Verify(mock => mock.addBinaryPath("dummyPath"), Times.Once); + tunnelMock.Verify(mock => mock.addBinaryPath("dummyPath", ""), Times.Once); tunnelMock.Verify(mock => mock.addBinaryArguments(It.IsRegex("-logFile \"" + logAbsolute + "\" .*")), Times.Once()); tunnelMock.Verify(mock => mock.Run("dummyKey", "", logAbsolute, "start"), Times.Once()); local.stop(); @@ -129,7 +129,7 @@ public void TestWorksWithBooleanOptions() tunnelMock.Setup(mock => mock.Run("dummyKey", "", logAbsolute, "start")); local.setTunnel(tunnelMock.Object); local.start(options); - tunnelMock.Verify(mock => mock.addBinaryPath(""), Times.Once); + tunnelMock.Verify(mock => mock.addBinaryPath("", ""), Times.Once); tunnelMock.Verify(mock => mock.addBinaryArguments(It.IsRegex("-vvv.*-force.*-forcelocal.*-forceproxy.*-onlyAutomate.*")), Times.Once()); tunnelMock.Verify(mock => mock.Run("dummyKey", "", logAbsolute, "start"), Times.Once()); local.stop(); @@ -152,7 +152,7 @@ public void TestWorksWithValueOptions() tunnelMock.Setup(mock =>mock.Run("dummyKey", "", logAbsolute, "start")); local.setTunnel(tunnelMock.Object); local.start(options); - tunnelMock.Verify(mock => mock.addBinaryPath(""), Times.Once); + tunnelMock.Verify(mock => mock.addBinaryPath("", ""), Times.Once); tunnelMock.Verify(mock => mock.addBinaryArguments( It.IsRegex("-localIdentifier.*dummyIdentifier.*dummyHost.*-proxyHost.*dummyHost.*-proxyPort.*dummyPort.*-proxyUser.*dummyUser.*-proxyPass.*dummyPass.*") ), Times.Once()); @@ -175,7 +175,7 @@ public void TestWorksWithCustomOptions() tunnelMock.Setup(mock => mock.Run("dummyKey", "", logAbsolute, "start")); local.setTunnel(tunnelMock.Object); local.start(options); - tunnelMock.Verify(mock => mock.addBinaryPath(""), Times.Once); + tunnelMock.Verify(mock => mock.addBinaryPath("", ""), Times.Once); tunnelMock.Verify(mock => mock.addBinaryArguments( It.IsRegex("-customBoolKey1.*-customBoolKey2.*-customKey1.*customValue1.*-customKey2.*customValue2.*") ), Times.Once()); @@ -200,7 +200,7 @@ public void TestCallsFallbackOnFailure() }); local.setTunnel(tunnelMock.Object); local.start(options); - tunnelMock.Verify(mock => mock.addBinaryPath(""), Times.Once); + tunnelMock.Verify(mock => mock.addBinaryPath("", ""), Times.Once); tunnelMock.Verify(mock => mock.addBinaryArguments(It.IsRegex("-logFile \"" + logAbsolute + "\" .*")), Times.Once()); tunnelMock.Verify(mock => mock.Run("dummyKey", "", logAbsolute, "start"), Times.Exactly(2)); tunnelMock.Verify(mock => mock.fallbackPaths(), Times.Once()); @@ -219,7 +219,7 @@ public void TestKillsTunnel() local.setTunnel(tunnelMock.Object); local.start(options); local.stop(); - tunnelMock.Verify(mock => mock.addBinaryPath(""), Times.Once); + tunnelMock.Verify(mock => mock.addBinaryPath("", ""), Times.Once); tunnelMock.Verify(mock => mock.addBinaryArguments(It.IsRegex("-logFile \"" + logAbsolute + "\" .*")), Times.Once()); tunnelMock.Verify(mock => mock.Run("dummyKey", "", logAbsolute, "start"), Times.Once()); } diff --git a/BrowserStackLocal/BrowserStackLocal/BrowserStackLocal.csproj b/BrowserStackLocal/BrowserStackLocal/BrowserStackLocal.csproj index 7711dbb..b0f902f 100644 --- a/BrowserStackLocal/BrowserStackLocal/BrowserStackLocal.csproj +++ b/BrowserStackLocal/BrowserStackLocal/BrowserStackLocal.csproj @@ -2,7 +2,7 @@ BrowserStack BrowserStackLocal - net20;netstandard2.0 + netstandard2.0 false BrowserStackLocal BrowserStackLocal diff --git a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs index 40ed6c1..4134cc2 100644 --- a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs +++ b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs @@ -8,6 +8,12 @@ using System.Security.Principal; using Newtonsoft.Json.Linq; + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; + namespace BrowserStack { public enum LocalState { Idle, Connecting, Connected, Error, Disconnected }; @@ -16,7 +22,11 @@ public class BrowserStackTunnel : IDisposable { static readonly string uname = Util.GetUName(); static readonly string binaryName = GetBinaryName(); - static readonly string downloadURL = "https://www.browserstack.com/local-testing/downloads/binaries/" + binaryName; + + private static string userAgent = ""; + private string sourceUrl = null; + private bool isFallbackEnabled = false; + private Exception downloadFailureException = null; static readonly string homepath = !IsWindows() ? Environment.GetFolderPath(Environment.SpecialFolder.Personal) : @@ -41,7 +51,7 @@ static bool IsDarwin(string osName) { return osName.Contains("darwin"); } - + static bool IsWindows() { @@ -84,8 +94,15 @@ static string GetBinaryName() return "BrowserStackLocal.exe"; } - public virtual void addBinaryPath(string binaryAbsolute) + public virtual void addBinaryPath(string binaryAbsolute, string accessKey, bool fallbackEnabled = false, Exception failureException = null) { + if (basePathsIndex == -1) + { + /* Called at most twice (primary & a fallback) */ + isFallbackEnabled = fallbackEnabled; + downloadFailureException = failureException; + fetchSourceUrl(accessKey); + } if (binaryAbsolute == null || binaryAbsolute.Trim().Length == 0) { binaryAbsolute = Path.Combine(basePaths[++basePathsIndex], binaryName); @@ -102,14 +119,19 @@ public virtual void addBinaryArguments(string binaryArguments) this.binaryArguments = binaryArguments; } - public BrowserStackTunnel() + public BrowserStackTunnel(string userAgentParam) { + userAgent = userAgentParam; localState = LocalState.Idle; output = new StringBuilder(); } public virtual void fallbackPaths() { + if (File.Exists(binaryAbsolute)) + { + File.Delete(binaryAbsolute); + } if (basePathsIndex >= basePaths.Length - 1) { throw new Exception("Binary not found or failed to launch. Make sure that BrowserStackLocal is not already running."); @@ -121,7 +143,7 @@ public virtual void fallbackPaths() public void modifyBinaryPermission() { if (!IsWindows()) - { + { try { using (Process proc = Process.Start("/bin/bash", $"-c \"chmod 0755 {this.binaryAbsolute}\"")) @@ -143,16 +165,58 @@ public void modifyBinaryPermission() } } + private string fetchSourceUrl(string accessKey) + { + var url = "https://local.browserstack.com/binary/api/v1/endpoint"; + + using (var client = new HttpClient()) + { + var data = new Dictionary + { + { "auth_token", accessKey } + }; + + if (isFallbackEnabled) + { + data["error_message"] = downloadFailureException.Message; + } + + var jsonData = JsonConvert.SerializeObject(data); + var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); + + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + if (isFallbackEnabled) + { + client.DefaultRequestHeaders.Add("X-Local-Fallback-Cloudflare", "true"); + } + + var response = client.PostAsync(url, content).Result; + + response.EnsureSuccessStatusCode(); + + var responseString = response.Content.ReadAsStringAsync().Result; + + var jsonResponse = JObject.Parse(responseString); + + if (jsonResponse["error"] != null) + { + throw new Exception((string)jsonResponse["error"]); + } + + sourceUrl = jsonResponse["data"]?["endpoint"]?.ToString(); + return sourceUrl; + } + } + public void downloadBinary() { string binaryDirectory = Path.Combine(this.binaryAbsolute, ".."); - //string binaryAbsolute = Path.Combine(binaryDirectory, binaryName); Directory.CreateDirectory(binaryDirectory); using (var client = new WebClient()) { - client.DownloadFile(downloadURL, this.binaryAbsolute); + client.DownloadFile(sourceUrl + "/" + binaryName, this.binaryAbsolute); } if (!File.Exists(binaryAbsolute)) diff --git a/BrowserStackLocal/BrowserStackLocal/Local.cs b/BrowserStackLocal/BrowserStackLocal/Local.cs index 5398c08..6c57251 100644 --- a/BrowserStackLocal/BrowserStackLocal/Local.cs +++ b/BrowserStackLocal/BrowserStackLocal/Local.cs @@ -14,6 +14,9 @@ public class Local private string argumentString = ""; private string customBinaryPath = ""; private string bindingVersion = ""; + private string userAgent = "browserstack-local-csharp"; + private bool isFallbackEnabled = false; + private Exception downloadFailureException = null; protected BrowserStackTunnel tunnel = null; private static KeyValuePair emptyStringPair = new KeyValuePair(); @@ -144,11 +147,59 @@ public static string GetVersionStringFromAssemblyEmbedded(string pAssemblyDispla public Local() { bindingVersion = GetVersionStringFromAssemblyEmbedded("BrowserStackLocal"); - tunnel = new BrowserStackTunnel(); + userAgent = userAgent + "/" + bindingVersion; + tunnel = new BrowserStackTunnel(userAgent); } - public void start(List> options) + + private void DownloadVerifyAndRunBinary() { tunnel.basePathsIndex = -1; + tunnel.addBinaryPath(customBinaryPath, accessKey, isFallbackEnabled, downloadFailureException); + try + { + while (true) + { + bool except = false; + try + { + tunnel.Run(accessKey, folder, customLogPath, "start"); + } + catch (System.ComponentModel.Win32Exception) + { + except = true; + } + catch (Exception e) + { + except = true; + Console.WriteLine(e.ToString()); + } + if (except) + { + tunnel.fallbackPaths(); + } + else + { + break; + } + } + } + catch (Exception err) + { + if (!isFallbackEnabled) + { + isFallbackEnabled = true; + downloadFailureException = err; + DownloadVerifyAndRunBinary(); + } + else + { + throw err; + } + } + } + + public void start(List> options) + { foreach (KeyValuePair pair in options) { string key = pair.Key; @@ -174,33 +225,9 @@ public void start(List> options) argumentString += "-logFile \"" + customLogPath + "\" "; argumentString += "--source \"c-sharp:" + bindingVersion + "\" "; - tunnel.addBinaryPath(customBinaryPath); tunnel.addBinaryArguments(argumentString); - while (true) - { - bool except = false; - try - { - tunnel.Run(accessKey, folder, customLogPath, "start"); - } - catch (System.ComponentModel.Win32Exception) - { - except = true; - } - catch (Exception e) - { - except = true; - Console.WriteLine(e.ToString()); - } - if (except) - { - tunnel.fallbackPaths(); - } - else - { - break; - } - } + + DownloadVerifyAndRunBinary(); } public void stop()