「session not created」でSeleniumが突然動かなくなる
RubyでSeleniumを使ったスクリプトを運用していると、ある日突然こんなエラーで止まることがあります。
Selenium::WebDriver::Error::SessionNotCreatedError: session not created: This version of ChromeDriver only supports Chrome version 124
Current browser version is 126.0.6478.126 with binary path...
原因ははっきりしていて、Chromeは自動更新でどんどんバージョンが上がっていくのに、ChromeDriverは手元に固定したバイナリのまま取り残されるからです。特にWindowsでタスクスケジューラに登録して定期実行しているようなスクリプトだと、ある朝いきなり失敗し始めて「昨日まで動いてたのに」と頭を抱えることになります。
毎回このエラーを見るたびに、
- 今のChromeのバージョンを確認する
- Chrome for Testingのサイトで対応するChromeDriverを探す
- zipをダウンロードして展開する
- 古いchromedriver.exeと差し替える
という手順を手でやるのは、地味に面倒です。しかも定期実行しているバッチだと「気づいた頃には数日分の実行が失敗し続けていた」ということにもなりかねません。そこで、このエラーを検知したら自動でChromeDriverを更新してリトライする仕組みをRubyの中に組み込みました。
まずはエラーメッセージから不一致を検知する
Selenium::WebDriver.forを呼んだときに例外が飛んできたら、そのメッセージにバージョン不一致特有の文言が含まれているかどうかで判定します。
def chromedriver_version_mismatch?(error)
message = error.message.to_s
message.include?('This version of ChromeDriver only supports Chrome version') ||
(message.include?('session not created') && message.include?('Current browser version is'))
end
ポイントは、エラークラスそのものではなく「メッセージ文字列に何が含まれているか」で判定していることです。Seleniumのバージョン不一致はSessionNotCreatedErrorという同じ例外クラスの中に、パーミッションエラーやプロファイルロックエラーなど別原因のケースも紛れ込んでいます。クラスだけで判定すると、それらまで巻き込んで「とりあえず自動更新」を発動させてしまい、無駄なダウンロードや余計な待ち時間が発生します。メッセージの文言まで見ることで、本当にバージョン不一致のときだけ更新処理に入るようにしています。
ドライバ生成処理に「不一致なら自動修復してリトライ」を仕込む
ドライバを起動する関数の中で、この判定関数を使って例外をキャッチし、必要なときだけ自動更新→再試行を行います。
def create_chrome_driver(options)
begin
Selenium::WebDriver.for(:chrome, options: options)
rescue => e
raise unless chromedriver_version_mismatch?(e)
warn "ChromeDriverとChromeのバージョン不一致を検出。自動更新します: #{e.message.lines.first}"
update_chromedriver_for_installed_chrome
Selenium::WebDriver.for(:chrome, options: options)
end
end
バージョン不一致以外の例外はraiseでそのまま再送出しているのがポイントです。「何でもリトライすれば直る」という雑な作りにすると、他の種類のエラー(ネットワーク断やプロファイル破損など)まで自動更新のロジックに巻き込まれてしまい、原因の切り分けが逆に難しくなります。自動修復は「これだと分かっている不具合」に対してだけピンポイントで発動させるのが安全です。
Chromeの実バージョンを取得する
自動更新には、まず今インストールされているChromeの正確なバージョンが必要です。Windowsであれば、実行ファイルのプロパティからPowerShell経由で取得できます。
require 'open3'
def installed_chrome_version
chrome_path = 'C:\Program Files\Google\Chrome\Application\chrome.exe'
return nil unless File.exist?(chrome_path)
stdout, _stderr, status = Open3.capture3(
'powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass',
'-Command', "(Get-Item '#{chrome_path}').VersionInfo.ProductVersion"
)
return nil unless status.success?
stdout.strip
end
chrome --versionをコマンドとして直接叩く方法もありますが、環境によってはコンソールウィンドウが一瞬表示されたり、出力フォーマットがバージョンによって微妙に揺れたりすることがあります。実行ファイルのファイルバージョン情報をPowerShellで直接読む方が、余計な出力に振り回されずに安定して取得できます。
Chrome for TestingのJSON APIで対応するChromeDriverを探す
ChromeDriverの配布元であるChrome for Testingは、既知の全バージョンとダウンロードURLをJSONで公開しています。取得したChromeのバージョン文字列と完全一致するエントリを探し、見つからなければメジャーバージョンだけで近いものを探すフォールバックを入れておきます。
$jsonUrl = 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json'
$data = Invoke-RestMethod -Uri $jsonUrl
$entry = $data.versions | Where-Object { $_.version -eq $chromeVersion } | Select-Object -First 1
if (-not $entry) {
$major = ($chromeVersion -split '\.')[0]
$entry = $data.versions | Where-Object { $_.version -like "$major.*" -and $_.downloads.chromedriver } | Select-Object -Last 1
}
if (-not $entry) { throw "No ChromeDriver entry found for Chrome $chromeVersion" }
完全一致のエントリが常にあるとは限りません。Chromeは細かいパッチバージョンで自動更新されますが、Chrome for Testing側のビルドがそのすべてのパッチバージョンに対して用意されているわけではないためです。メジャーバージョンさえ合っていればChromeDriverはほぼ問題なく動くので、フォールバックとして許容しています。
ダウンロード・展開・差し替え、そしてRuby側での再試行
対応するダウンロードURLが見つかったら、一時フォルダにzipを落として展開し、既存のchromedriver.exeをバックアップしてから差し替えます。
$download = $entry.downloads.chromedriver | Where-Object { $_.platform -eq 'win64' } | Select-Object -First 1
$tempRoot = Join-Path $env:TEMP "chromedriver-$($entry.version)-$(Get-Date -Format yyyyMMddHHmmss)"
New-Item -ItemType Directory -Path $tempRoot | Out-Null
$zipPath = Join-Path $tempRoot 'chromedriver-win64.zip'
Invoke-WebRequest -Uri $download.url -OutFile $zipPath
Expand-Archive -Path $zipPath -DestinationPath $tempRoot
$newDriver = Get-ChildItem -Path $tempRoot -Recurse -Filter chromedriver.exe | Select-Object -First 1
if (Test-Path $driverPath) {
$backup = $driverPath + '.bak-' + (Get-Date -Format yyyyMMdd-HHmmss)
Move-Item -LiteralPath $driverPath -Destination $backup
}
Copy-Item -LiteralPath $newDriver.FullName -Destination $driverPath
差し替え前に古いファイルを削除せず、タイムスタンプ付きでリネームして残しているのがちょっとしたコツです。万一ダウンロードしたバイナリに問題があっても、すぐ元に戻せます。
これをRuby側からOpen3で呼び出し、成功したら差し替え後のドライバパスをSeleniumに教えてあげます。
def update_chromedriver_for_installed_chrome
chrome_version = installed_chrome_version
raise 'Chromeのバージョンを取得できませんでした' if chrome_version.nil? || chrome_version.empty?
kill_chromedriver_processes # 古いchromedriver.exeプロセスが残っているとファイルを上書きできないため
script = build_update_powershell_script(chrome_version)
_stdout, stderr, status = Open3.capture3(
'powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script
)
raise "ChromeDriver自動更新に失敗しました: #{stderr.strip}" unless status.success?
driver_path = File.expand_path('./bin/chromedriver.exe')
Selenium::WebDriver::Chrome.driver_path = driver_path if File.exist?(driver_path)
true
end
更新前に必ずkill_chromedriver_processesでプロセスを止めておく点も地味に重要です。Windowsは実行中のexeファイルを上書きできないため、古いドライバプロセスが生きたままだとCopy-Itemが失敗します。
まとめ
- ChromeDriverのバージョン不一致は、エラーメッセージの文字列判定で検知するのが実用的。例外クラスだけでは他原因のエラーと区別できない
- 不一致と判定できたときだけ自動更新し、それ以外の例外はそのまま再送出する。何でもリトライする作りは事故のもと
- Chrome for TestingのJSON APIを使えば、インストール済みChromeの正確なバージョンに対応するChromeDriverをプログラムから特定できる
- 更新前にプロセスをkillし、更新後は旧ファイルをリネーム退避してから差し替えると、Windows特有のファイルロックや失敗時の切り戻しに強くなる


コメント