diff --git a/WebDriverManager.Tests/ChromeConfigTests.cs b/WebDriverManager.Tests/ChromeConfigTests.cs index 85e2c4a..3c41c88 100644 --- a/WebDriverManager.Tests/ChromeConfigTests.cs +++ b/WebDriverManager.Tests/ChromeConfigTests.cs @@ -16,7 +16,7 @@ public void VersionTest() } [Fact] - public void DriverDownloadTest() + public void DriverDownloadLatestTest() { new DriverManager().SetUpDriver(new ChromeConfig()); Assert.NotEmpty(WebDriverFinder.FindFile(GetBinaryName())); diff --git a/WebDriverManager/Clients/ChromeForTestingClient.cs b/WebDriverManager/Clients/ChromeForTestingClient.cs new file mode 100644 index 0000000..0a40846 --- /dev/null +++ b/WebDriverManager/Clients/ChromeForTestingClient.cs @@ -0,0 +1,59 @@ +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using WebDriverManager.Models.Chrome; + +namespace WebDriverManager.Clients +{ + public static class ChromeForTestingClient + { + private static readonly string BaseUrl = "https://googlechromelabs.github.io/chrome-for-testing/"; + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + private static readonly HttpClient _httpClient; + + static ChromeForTestingClient() + { + _httpClient = new HttpClient + { + BaseAddress = new Uri(BaseUrl) + }; + } + + public static ChromeVersions GetKnownGoodVersionsWithDownloads() + { + return GetResultFromHttpTask( + _httpClient.GetAsync("known-good-versions-with-downloads.json") + ); + } + + public static ChromeVersions GetLastKnownGoodVersions() + { + return GetResultFromHttpTask( + _httpClient.GetAsync("last-known-good-versions-with-downloads.json") + ); + } + + /// + /// Get a HTTP result without causing any deadlocks + /// See: https://learn.microsoft.com/en-us/archive/blogs/jpsanders/asp-net-do-not-use-task-result-in-main-context + /// + /// The type of result to convert the HTTP response to + /// The task to run + private static TResult GetResultFromHttpTask(Task taskToRun) + where TResult : class + { + var httpTask = Task.Run(() => taskToRun); + httpTask.Wait(); + + var readStringTask = Task.Run(() => httpTask.Result.Content.ReadAsStringAsync()); + readStringTask.Wait(); + + return JsonSerializer.Deserialize(readStringTask.Result, JsonSerializerOptions); + } + } +} diff --git a/WebDriverManager/DriverConfigs/Impl/ChromeConfig.cs b/WebDriverManager/DriverConfigs/Impl/ChromeConfig.cs index 4fa914c..3fb3d45 100644 --- a/WebDriverManager/DriverConfigs/Impl/ChromeConfig.cs +++ b/WebDriverManager/DriverConfigs/Impl/ChromeConfig.cs @@ -1,18 +1,31 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Runtime.InteropServices; +using WebDriverManager.Clients; using WebDriverManager.Helpers; +using WebDriverManager.Models.Chrome; namespace WebDriverManager.DriverConfigs.Impl { public class ChromeConfig : IDriverConfig { private const string BaseVersionPatternUrl = "https://chromedriver.storage.googleapis.com//"; - private const string LatestReleaseVersionUrl = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"; + private const string ExactReleaseVersionPatternUrl = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_"; - private const string ExactReleaseVersionPatternUrl = - "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_"; + /// + /// The minimum version required to download chrome drivers from Chrome for Testing API's + /// + private static readonly Version MinChromeForTestingDriverVersion = new Version("115.0.5763.0"); + + /// + /// The minimum version of chrome driver required to reference download URLs via the "arm64" extension + /// + private static readonly Version MinArm64ExtensionVersion = new Version("106.0.5249.61"); + + private ChromeVersionInfo _chromeVersionInfo; + private string _chromeVersion; public virtual string GetName() { @@ -31,23 +44,14 @@ public virtual string GetUrl64() private string GetUrl() { -#if NETSTANDARD - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // Handle newer versions of chrome driver only being available for download via the Chrome for Testing API's + // whilst retaining backwards compatibility for older versions of chrome/chrome driver. + if (_chromeVersionInfo != null) { - var architectureExtension = - RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 - ? "_arm64" - : "64"; - return $"{BaseVersionPatternUrl}chromedriver_mac{architectureExtension}.zip"; + return GetUrlFromChromeForTestingApi(); } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return $"{BaseVersionPatternUrl}chromedriver_linux64.zip"; - } -#endif - - return $"{BaseVersionPatternUrl}chromedriver_win32.zip"; + return GetUrlFromChromeStorage(); } public virtual string GetBinaryName() @@ -63,10 +67,49 @@ public virtual string GetBinaryName() public virtual string GetLatestVersion() { - return GetLatestVersion(LatestReleaseVersionUrl); + var chromeReleases = ChromeForTestingClient.GetLastKnownGoodVersions(); + var chromeStable = chromeReleases.Channels.Stable; + + _chromeVersionInfo = new ChromeVersionInfo + { + Downloads = chromeStable.Downloads + }; + + return chromeStable.Version; } - private static string GetLatestVersion(string url) + public virtual string GetMatchingBrowserVersion() + { + var rawChromeBrowserVersion = GetRawBrowserVersion(); + if (string.IsNullOrEmpty(rawChromeBrowserVersion)) + { + throw new Exception("Not able to get chrome version or not installed"); + } + + var chromeVersion = VersionHelper.GetVersionWithoutRevision(rawChromeBrowserVersion); + + // Handle downloading versions of the chrome webdriver less than what's supported by the Chrome for Testing known good versions API + // See https://googlechromelabs.github.io/chrome-for-testing for more info + var matchedVersion = new Version(rawChromeBrowserVersion); + if (matchedVersion < MinChromeForTestingDriverVersion) + { + var url = ExactReleaseVersionPatternUrl.Replace("", chromeVersion); + _chromeVersion = GetVersionFromChromeStorage(url); + } + else + { + _chromeVersion = GetVersionFromChromeForTestingApi(chromeVersion).Version; + } + + return _chromeVersion; + } + + /// + /// Retrieves a chrome driver version string from https://chromedriver.storage.googleapis.com + /// + /// The request URL + /// A chrome driver version string + private static string GetVersionFromChromeStorage(string url) { var uri = new Uri(url); var webRequest = WebRequest.Create(uri); @@ -84,17 +127,85 @@ private static string GetLatestVersion(string url) } } - public virtual string GetMatchingBrowserVersion() + /// + /// Retrieves a download URL for a chrome driver from the https://chromedriver.storage.googleapis.com API's + /// + /// A chrome driver download URL + private string GetUrlFromChromeStorage() { - var rawChromeBrowserVersion = GetRawBrowserVersion(); - if (string.IsNullOrEmpty(rawChromeBrowserVersion)) +#if NETSTANDARD + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - throw new Exception("Not able to get chrome version or not installed"); + // Handle older versions of chrome driver arm64 builds that are tagged with 64_m1 instead of arm64. + // See: https://chromedriver.storage.googleapis.com/index.html?path=106.0.5249.21/ + var useM1Prefix = new Version(_chromeVersion) < MinArm64ExtensionVersion; + var armArchitectureExtension = useM1Prefix + ? "64_m1" + : "_arm64"; + + var architectureExtension = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 + ? armArchitectureExtension + : "64"; + + return $"{BaseVersionPatternUrl}chromedriver_mac{architectureExtension}.zip"; } - var chromeBrowserVersion = VersionHelper.GetVersionWithoutRevision(rawChromeBrowserVersion); - var url = ExactReleaseVersionPatternUrl.Replace("", chromeBrowserVersion); - return GetLatestVersion(url); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return $"{BaseVersionPatternUrl}chromedriver_linux64.zip"; + } +#endif + + return $"{BaseVersionPatternUrl}chromedriver_win32.zip"; + } + + /// + /// Retrieves a chrome driver version string from https://googlechromelabs.github.io/chrome-for-testing + /// + /// The desired version to download + /// Chrome driver version info (version number, revision number, download URLs) + private ChromeVersionInfo GetVersionFromChromeForTestingApi(string noRevisionVersion) + { + var knownGoodVersions = ChromeForTestingClient.GetKnownGoodVersionsWithDownloads(); + + // Pull latest patch version + _chromeVersionInfo = knownGoodVersions.Versions.LastOrDefault( + cV => cV.Version.Contains(noRevisionVersion) + ); + + return _chromeVersionInfo; + } + + /// + /// Retrieves a chrome driver download URL from Chrome for Testing API's + /// + /// A chrome driver download URL + private string GetUrlFromChromeForTestingApi() + { + var platform = "win32"; + +#if NETSTANDARD + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + platform = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 + ? "mac-arm64" + : "mac-x64"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + platform = "linux64"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + platform = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? "win64" + : "win32"; + } +#endif + var result = _chromeVersionInfo.Downloads.ChromeDriver + .FirstOrDefault(driver => driver.Platform == platform); + + return result.Url; } private string GetRawBrowserVersion() diff --git a/WebDriverManager/Models/Chrome/ChromeDownload.cs b/WebDriverManager/Models/Chrome/ChromeDownload.cs new file mode 100644 index 0000000..670b109 --- /dev/null +++ b/WebDriverManager/Models/Chrome/ChromeDownload.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace WebDriverManager.Models.Chrome +{ + public class ChromeDownload + { + public IEnumerable ChromeDriver { get; set; } + } +} diff --git a/WebDriverManager/Models/Chrome/ChromePlatformInfo.cs b/WebDriverManager/Models/Chrome/ChromePlatformInfo.cs new file mode 100644 index 0000000..03f2497 --- /dev/null +++ b/WebDriverManager/Models/Chrome/ChromePlatformInfo.cs @@ -0,0 +1,9 @@ +namespace WebDriverManager.Models.Chrome +{ + public class ChromePlatformInfo + { + public string Platform { get; set; } + + public string Url { get; set; } + } +} diff --git a/WebDriverManager/Models/Chrome/ChromeReleaseChannels.cs b/WebDriverManager/Models/Chrome/ChromeReleaseChannels.cs new file mode 100644 index 0000000..3e02bbd --- /dev/null +++ b/WebDriverManager/Models/Chrome/ChromeReleaseChannels.cs @@ -0,0 +1,13 @@ +namespace WebDriverManager.Models.Chrome +{ + public class ChromeReleaseChannels + { + public ChromeReleaseTrack Stable { get; set; } + + public ChromeReleaseTrack Beta { get; set; } + + public ChromeReleaseTrack Dev { get; set; } + + public ChromeReleaseTrack Canary { get; set; } + } +} diff --git a/WebDriverManager/Models/Chrome/ChromeReleaseTrack.cs b/WebDriverManager/Models/Chrome/ChromeReleaseTrack.cs new file mode 100644 index 0000000..67a575a --- /dev/null +++ b/WebDriverManager/Models/Chrome/ChromeReleaseTrack.cs @@ -0,0 +1,13 @@ +namespace WebDriverManager.Models.Chrome +{ + public class ChromeReleaseTrack + { + public string Channel { get; set; } + + public string Version { get; set; } + + public string Revision { get; set; } + + public ChromeDownload Downloads { get; set; } + } +} diff --git a/WebDriverManager/Models/Chrome/ChromeVersionInfo.cs b/WebDriverManager/Models/Chrome/ChromeVersionInfo.cs new file mode 100644 index 0000000..9e5135f --- /dev/null +++ b/WebDriverManager/Models/Chrome/ChromeVersionInfo.cs @@ -0,0 +1,11 @@ +namespace WebDriverManager.Models.Chrome +{ + public class ChromeVersionInfo + { + public string Version { get; set; } + + public string Revision { get; set; } + + public ChromeDownload Downloads { get; set; } + } +} diff --git a/WebDriverManager/Models/Chrome/ChromeVersions.cs b/WebDriverManager/Models/Chrome/ChromeVersions.cs new file mode 100644 index 0000000..b042b5f --- /dev/null +++ b/WebDriverManager/Models/Chrome/ChromeVersions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace WebDriverManager.Models.Chrome +{ + public class ChromeVersions + { + public string Timestamp { get; set; } + + public ChromeReleaseChannels Channels { get; set; } + + public IEnumerable Versions { get; set; } + } +} diff --git a/WebDriverManager/WebDriverManager.csproj b/WebDriverManager/WebDriverManager.csproj index d92c7d5..9071142 100644 --- a/WebDriverManager/WebDriverManager.csproj +++ b/WebDriverManager/WebDriverManager.csproj @@ -21,6 +21,7 @@ +