Search code examples
c#seleniumselenium-chromedriver

Try Selenium Download use of Latest chromedriver.exe in C# if it Fails to Instanciate to fix Session not created Exception


In my Selenium UI tests that use the NuGet package Selenium.WebDriver I can already detect if I need to download a newer chromedriver.exe. It is if new ChromeDriver() fails:

try
{
    this.driver = new ChromeDriver(AppDomain.CurrentDomain.BaseDirectory, options);
}
catch (Exception ex)
{
    throw new Exception($"Failed to setup chromedriver. Consider killing chromedriver.exe and downloading and updating it from https://chromedriver.chromium.org/downloads and update it in project with copy to output. Exception: {ex}");
}

The Exception Message is:

session not created: This version of ChromeDriver only supports Chrome version [version]

Also I can already detect if chromedriver wasn't yet downloaded simply by checkcing:

var chromedriverPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "chromedriver.exe");

if(!File.Exists(chromedriverPath))
{
    throw new Exception($"chromedriver.exe not found at location {chromedriverPath}. Download it from https://chromedriver.chromium.org/downloads and update it in project with copy to output");
}

I am looking for an recommended way to fix this error automatically.

Something like (after detecting chromedriver.exe missing or outdated):

  1. Find out what latest version is and kill dead chromedriver.exe process
  2. Download and unpack e.g. from https://chromedriver.chromium.org/downloads
  3. Replace chromedriver.exe in AppDomain.CurrentDomain.BaseDirectory

Bonus:

  • The Exception message mentions a major version of chromedriver where there could be a fallback to use this version if latests also fails.
  • Detect your current OS an download the right package per OS. Especially interested in compatibility with macOS

It's ok if it then still fails but it should at least try to just update chromedriver.exe to latest because that usually fixes the problem. In the past I've tried packages but often those packages were just shipping the chromedriver.exe file and were badly maintained. Also it would need to be a trusted package with many downloads for me consider it again. Also the cross OS thing is a topic there.

If you write a great answer here consider creating a package out of it or at least allowing to use your code for that purpose :)


Solution

  • I found this great repo by Niels Swimberghe @Swimburger https://github.com/Swimburger/DownloadChromeDriverSample

    Here a link to the class to copy paste: https://raw.githubusercontent.com/Swimburger/DownloadChromeDriverSample/main/Console/ChromeDriverInstaller.cs

    Usage:

    //autofixing chromedriver
    var chromedriverInstaller = new ChromeDriverInstaller();
    chromedriverInstaller.Install().Wait();
    
    try
    {
        this.driver = new ChromeDriver(AppDomain.CurrentDomain.BaseDirectory, options);
    }
    catch (Exception ex)
    {
        throw new Exception($"Failed to setup chromedriver. Consider killing chromedriver.exe and downloading and updating it from https://chromedriver.chromium.org/downloads and update it in project with copy to output. Exception: {ex}");
    }
    

    It is not perfect because ideally it would only run if the test fails.

    Update

    Meanwhile I extended the script a few times and this question received the "Popular Question" badge so I thought I paste my current implementation...

    Code (sadly windows specific):

    public class KnownGoodVersionsResponse
    {
        public DateTime timestamp { get; set; }
        public List<ChromeVersionDetails> versions { get; set; }
    }
    
    public class ChromeVersionDetails
    {
        public string version { get; set; }
        public string revision { get; set; }
    }
    
    //https://raw.githubusercontent.com/Swimburger/DownloadChromeDriverSample/main/Console/ChromeDriverInstaller.cs
    public class ChromeDriverInstaller
    {
        private static readonly HttpClient httpClient = new HttpClient();
    
        public Task Install() => Install(null, false);
        public Task Install(string chromeVersion) => Install(chromeVersion, false);
        public Task Install(bool forceDownload) => Install(null, forceDownload);
    
        public async Task Install(string chromeVersion, bool forceDownload)
        {
            // Instructions from https://chromedriver.chromium.org/downloads/version-selection
            //   First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81.
            if (chromeVersion == null)
            {
                chromeVersion = await GetLocalChromeVersionAsync();
            }
    
            var chromeDriverVersion = await GetMatchingClosestAvailableChromeVersionAsync(chromeVersion);
    
    
            string zipName, driverName, bitVersionOs;
            GetZipNameAndDriverName(out zipName, out driverName, out bitVersionOs);
    
            string targetPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    
            targetPath = Path.Combine(targetPath, driverName);
            if (!forceDownload && File.Exists(targetPath))
            {
                using var process = Process.Start(
                    new ProcessStartInfo
                    {
                        FileName = targetPath,
                        ArgumentList = { "--version" },
                        UseShellExecute = false,
                        CreateNoWindow = true,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                    }
                );
                string existingChromeDriverVersion = await process.StandardOutput.ReadToEndAsync();
                string error = await process.StandardError.ReadToEndAsync();
                await process.WaitForExitAsync();
                process.Kill(true);
    
                // expected output is something like "ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784})"
                // the following line will extract the version number and leave the rest
                existingChromeDriverVersion = existingChromeDriverVersion.Split(" ")[1];
                if (chromeDriverVersion == existingChromeDriverVersion)
                {
                    return;
                }
    
                if (!string.IsNullOrEmpty(error))
                {
                    throw new Exception($"Failed to execute {driverName} --version");
                }
            }
    
            //   and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE".
            //   For example, with Chrome version 115.0.5790.171 (171 not yet available), you'd get a URL "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5790.170/win64/chromedriver-win64.zip".
            var chromeDriverDownloadUrl = $"https://storage.googleapis.com/chrome-for-testing-public/{chromeDriverVersion}/{bitVersionOs}/{zipName}";
            var driverZipResponse = await httpClient.GetAsync(chromeDriverDownloadUrl);
            if (!driverZipResponse.IsSuccessStatusCode)
            {
                if (driverZipResponse.StatusCode == HttpStatusCode.NotFound)
                {
                    throw new Exception($"ChromeDriver version not found for Chrome version {chromeDriverVersion}");
                }
                else
                {
                    throw new Exception($"ChromeDriver download request failed with status code: {driverZipResponse.StatusCode}, reason phrase: {driverZipResponse.ReasonPhrase}");
                }
            }
    
            // this reads the zipfile as a stream, opens the archive, 
            // and extracts the chromedriver executable to the targetPath without saving any intermediate files to disk
            using (var zipFileStream = await driverZipResponse.Content.ReadAsStreamAsync())
            using (var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
            using (var chromeDriverWriter = new FileStream(targetPath, FileMode.Create))
            {
                var entry = zipArchive.GetEntry($"chromedriver-{bitVersionOs}/{driverName}");
                using Stream chromeDriverStream = entry.Open();
                await chromeDriverStream.CopyToAsync(chromeDriverWriter);
            }
    
            // on Linux/macOS, you need to add the executable permission (+x) to allow the execution of the chromedriver
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                using var process = Process.Start(
                    new ProcessStartInfo
                    {
                        FileName = "chmod",
                        ArgumentList = { "+x", targetPath },
                        UseShellExecute = false,
                        CreateNoWindow = true,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                    }
                );
                string error = await process.StandardError.ReadToEndAsync();
                await process.WaitForExitAsync();
                process.Kill(true);
    
                if (!string.IsNullOrEmpty(error))
                {
                    throw new Exception("Failed to make chromedriver executable");
                }
            }
        }
    
        private static void GetZipNameAndDriverName(out string zipName, out string driverName, out string bitVersionOs)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                zipName = "chromedriver-win64.zip";
                driverName = "chromedriver.exe";
                bitVersionOs = "win64";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                zipName = "chromedriver-linux64.zip";
                driverName = "chromedriver";
                bitVersionOs = "linux64";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                zipName = "chromedriver-mac64.zip";
                driverName = "chromedriver";
                bitVersionOs = "mac-arm64";
            }
            else
            {
                throw new PlatformNotSupportedException("Your operating system is not supported.");
            }
        }
    
        private async Task<string> GetMatchingClosestAvailableChromeVersionAsync(string chromeVersion)
        {
            var chromeVersionBase = chromeVersion.Substring(0, chromeVersion.LastIndexOf('.'));
            var initialMinorVersion = int.Parse(chromeVersion.Split('.').Last());
            
            string zipName, driverName, bitVersionOs;
            GetZipNameAndDriverName(out zipName, out driverName, out bitVersionOs);
            
            var retryCount = 0;
            const int maxRetry = 3;
            while (retryCount < maxRetry)
            {
                var adjustedMinorVersion = initialMinorVersion - retryCount;
                var adjustedVersion = $"{chromeVersionBase}.{adjustedMinorVersion}";
    
                try
                {
                    var chromeDriverVersionsResponse = await httpClient.GetAsync("https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json");
                    chromeDriverVersionsResponse.EnsureSuccessStatusCode();
    
                    var content = await chromeDriverVersionsResponse.Content.ReadAsStringAsync();
                    var knownGoodVersionsResponse = JsonConvert.DeserializeObject<KnownGoodVersionsResponse>(content);
    
                    var matchingVersion = knownGoodVersionsResponse.versions.LastOrDefault(x => x.version.StartsWith(adjustedVersion));
                    if (matchingVersion != null)
                    {
                        var chromeDriverDownloadUrl = $"https://storage.googleapis.com/chrome-for-testing-public/{matchingVersion.version}/{bitVersionOs}/{zipName}";
                        var driverZipResponse = await httpClient.GetAsync(chromeDriverDownloadUrl);
                        if (driverZipResponse.IsSuccessStatusCode)
                        {
                            return matchingVersion.version;
                        }
                    }
                }
                catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    // Log or handle the NotFound case if necessary.
                }
                catch (Exception ex)
                {
                    // Log or handle other exceptions.
                    Console.WriteLine($"An error occurred: {ex.Message}");
                }
    
                retryCount++;
            }
    
            throw new Exception($"Unable to find a downloadable ChromeDriver version for Chrome version {chromeVersion} after {maxRetry} attempts.");
        }
    
        public async Task<string> GetLocalChromeVersionAsync()
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                string chromePath = (string)Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe", null, null);
                if (chromePath == null)
                {
                    throw new Exception("Google Chrome not found in registry");
                }
    
                var fileVersionInfo = FileVersionInfo.GetVersionInfo(chromePath);
                return fileVersionInfo.FileVersion;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                try
                {
                    using var process = Process.Start(
                        new ProcessStartInfo
                        {
                            FileName = "google-chrome",
                            ArgumentList = { "--product-version" },
                            UseShellExecute = false,
                            CreateNoWindow = true,
                            RedirectStandardOutput = true,
                            RedirectStandardError = true,
                        }
                    );
                    string output = await process.StandardOutput.ReadToEndAsync();
                    string error = await process.StandardError.ReadToEndAsync();
                    await process.WaitForExitAsync();
                    process.Kill(true);
    
                    if (!string.IsNullOrEmpty(error))
                    {
                        throw new Exception(error);
                    }
    
                    return output;
                }
                catch (Exception ex)
                {
                    throw new Exception("An error occurred trying to execute 'google-chrome --product-version'", ex);
                }
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                try
                {
                    using var process = Process.Start(
                        new ProcessStartInfo
                        {
                            FileName = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                            ArgumentList = { "--version" },
                            UseShellExecute = false,
                            CreateNoWindow = true,
                            RedirectStandardOutput = true,
                            RedirectStandardError = true,
                        }
                    );
                    string output = await process.StandardOutput.ReadToEndAsync();
                    string error = await process.StandardError.ReadToEndAsync();
                    await process.WaitForExitAsync();
                    process.Kill(true);
    
                    if (!string.IsNullOrEmpty(error))
                    {
                        throw new Exception(error);
                    }
    
                    output = output.Replace("Google Chrome ", "");
                    return output;
                }
                catch (Exception ex)
                {
                    throw new Exception($"An error occurred trying to execute '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version'", ex);
                }
            }
            else
            {
                throw new PlatformNotSupportedException("Your operating system is not supported.");
            }
        }
    }
    

    Use it like this:

    //autofixing chromedriver
    var chromedriverInstaller = new ChromeDriverInstaller();
    await chromedriverInstaller.Install();