Seleniumのchromedriver.exeとmsedgedriver.exeを自動更新する

# はじめに
ブラウザ自動テストのデファクトスタンダードである`Selenium`。
悩ましいことに操作対象のブラウザのバージョンに合わせて`WebDriver`を更新しなければいけません。

今回対象とするブラウザは`Google Chrome`と`Chromium版Edge`です。
いずれも約4週間に一度のペースで自動更新されるため、「自動更新しよう！」ということになります。

`chromedriver`の更新方法と、`msedgedriver`の更新方法をまとめました。

:::note info
2023/09月 v116 以降の chromedriver の公開URLが変更されたため、記事の内容を修正しました。
:::

# 実装方針
それぞれの`WebDriver`のバージョン情報やバイナリを取得できる**WebAPIが公開されています**。
それを組み合わせることで自動更新を実現します。
詳細はそれぞれ以下のパスを参照してください：

* `chromedriver`は[こちら](https://googlechromelabs.github.io/chrome-for-testing/)
* `msedgedriver`は[こちら](https://msedgedriver.azureedge.net/)

上記URLをみればわかる通り、やりたいことに合わせて色々アレンジできそうです。
※注意：以下、WindowsOS用のWin64バイナリを取得している例なので、適宜アレンジが必要です。

## chromedriver.exe
0. 操作対象となる`chrome.exe`のメジャーバージョンを取得する
Windowsだったらこのパスにあるはず：C:\Program Files\Google\Chrome\Application\chrome.exe

0. 以下のURIによって、メジャーバージョンをもとに最新の`WebDriver`のフルバージョンを取得する
※例えば v116 の場合：
https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_116
※注意）以下のURIは v115 以降は無効です：
~~https://chromedriver.storage.googleapis.com/LATEST_RELEASE_114~~
2. 以下のURIによって、`WebDriver`のフルバージョンをもとにzipファイルをダウンロードする
※例えば 116.0.5845.96 の場合：
https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/116.0.5845.96/win64/chromedriver-win64.zip
※注意）以下のURIは v115 以降は無効です：
~~https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_win32.zip~~

3. ダウンロードしたzipファイルを展開する

## msedgedriver.exe

0. 操作対象となる`msedge.exe`のメジャーバージョンを取得する
Windowsだったらこのパスにあるはず：C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe

1. 以下のURIによって、メジャーバージョンをもとに最新の`WebDriver`のフルバージョンを取得する
※例えば v116 の場合：
https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/LATEST_RELEASE_116

2. 以下のURIによって、`WebDriver`のフルバージョンをもとにzipファイルをダウンロードする
※例えば 116.0.1938.69 の場合：
https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/116.0.1938.69/edgedriver_win64.zip

3. ダウンロードしたzipファイルを展開する

# 実装例（C#）
`C#`で実装するとしたらこんな感じです。
注意：`msedgedriver`のバージョン取得結果の末尾に改行が入ってしまうのでトリミング処理が必要です。

```csharp
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace ConsoleApp
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            await UpdateChromeDriverAsync();
            await UpdateMsEdgeDriver();
        }

        public static async Task UpdateChromeDriverAsync()
        {
            // exeを配置したいフォルダを指定
            var directory = "chromedriver_" + Guid.NewGuid().ToString();

            var path = @"C:\Program Files\Google\Chrome\Application\chrome.exe";
            var majorversion = FileVersionInfo.GetVersionInfo(path).FileMajorPart;

            var versionUri = new Uri($@"https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_{majorversion}");
            var version = await GetResponseAsStringAsync(versionUri);

            var driverUri = new Uri($@"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/{version}/win64/chromedriver-win64.zip");
            await DownloadAsync(driverUri, directory);
        }

        public static async Task UpdateMsEdgeDriver()
        {
            // exeを配置したいフォルダを指定
            var directory = "msedgedriver_" + Guid.NewGuid().ToString();

            var path = @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe";
            var majorversion = FileVersionInfo.GetVersionInfo(path).FileMajorPart;

            var versionUri = new Uri($@"https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/LATEST_RELEASE_{majorversion}");
            var version = await GetResponseAsStringAsync(versionUri);

            // 末尾に改行コードが入ってしまうので削除する
            version = version.Trim();

            var driverUri = new Uri($@"https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/{version}/edgedriver_win64.zip");
            await DownloadAsync(driverUri, directory);
        }

        private static async Task<string> GetResponseAsStringAsync(Uri uri)
        {
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
            using (var httpClient = new HttpClient())
            using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
            using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
            {
                using (var content = response.Content)
                {
                    return await content.ReadAsStringAsync();
                }
            }
        }

        private static async Task DownloadAsync(Uri uri, string extractDirectory)
        {
            if (!Directory.Exists(extractDirectory))
            {
                _ = Directory.CreateDirectory(extractDirectory);
            }

            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
            using (var httpClient = new HttpClient())
            using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
            using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
            using (var content = response.Content)
            using (var stream = await content.ReadAsStreamAsync())
            using (var archive = new ZipArchive(stream))
            {
                UnZip(extractDirectory, archive);
            }
        }

        private static void UnZip(string extractDirectory, ZipArchive archive)
        {
            foreach (var entry in archive.Entries.Where(x => Path.GetExtension(x.Name) == ".exe"))
            {
                var path = Path.GetFullPath(Path.Combine(extractDirectory, Path.GetFileName(entry.FullName)));
                using (var stream = entry.Open())
                using (var writer = new FileStream(path, FileMode.Create, FileAccess.Write))
                {
                    var buffer = new byte[2048];
                    int len;
                    while ((len = stream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        writer.Write(buffer, 0, len);
                    }
                }
            }
        }
    }
}

```
## ブラウザパスの取得について
ブラウザがインストールされいているパスが環境によって異なることがあります。
そのため、更新ロジックのバージョン取得の部分が適切に動作しないことがあります。

その対策として、PATH でブラウザ指定してヘッドレスモードで一度起動し、
そのプロセスからファイルパスを取得する方法を提示します。
私はこの手法で driver を安定して更新することができています。
```cs
public static class BrowserFile
{
    public static string GetChromePath()
    {
        return GetPath("chrome");
    }
    public static string GetMsedgePath()
    {
        return GetPath("msedge");
    }

    private static string GetPath(string cmd)
    {
        try
        {
            string path;
            using (var p = Process.Start(cmd, "--headless"))
            {
                _ = p.WaitForInputIdle();
                path = p.MainModule.FileName;
                p.Kill();
            }

            return path;
        }
        catch
        {
            return null;
        }
    }
}
```
# おわりに
同じ目的の記事がたくさんありましたが、
本質的なところを端的にまとめている記事がなかったので、本質的なところを端的にまとめてみました。
また、今後需要が伸びてくるであろう`msedgedriver`にも言及し、競合記事との差別化を図りました。