.NET MAUI + macOS で Selenium を使うまでの苦労話
https://scrapbox.io/files/625252db3db3bf001d7378b5.png
結論 (TL;DR)
ターゲットフレームワークが net6.0-maccatalyst の場合、 System.Net.Http.HttpClientHandler の Proxy などのメンバにアクセスすると PlatformNotSupportedException が発生するようになっている
直接の原因: https://github.com/xamarin/xamarin-macios/blob/e191c613ceaca400cd1496c2bbcbe739dc97b95e/src/Foundation/NSUrlSessionHandler.cs#L532-L613
数日後に改善されたけど、 Proxy をセットすると例外が出るのは変わらない
不幸なことに、 Selenium はユーザがProxyを使うつもりがなくても HttpClientHandler.Proxy に null を代入するつくりになっている
https://github.com/SeleniumHQ/selenium/blob/a22e15736dd4f81fe0d1a7d07fe556d4ecae50ad/dotnet/src/webdriver/Remote/HttpCommandExecutor.cs#L254
ここが null の時や HttpClientHandler.SupportsProxy が false の時は代入しないように変更されたらなぁ
ChromeDriver, ChromiumDriver, DriverServiceCommandExecutor, HttpCommandExecutor を自作することで Proxy の代入を回避することは可能
/icons/hr.icon
macOSでもコンソールアプリなら普通に Selenium が使える。
(dotnet add package で --version を指定するのは要らないかも)
code:sh
dotnet new console --language "F#"
dotnet add package Selenium.WebDriver --version 4.1.0
dotnet add package Selenium.WebDriver.ChromeDriver --version 100.0.4896.6000
code:Program.fs
open System
open OpenQA.Selenium.Chrome
let driver = new ChromeDriver();
driver.Navigate().GoToUrl("http://www.google.co.jp/")
code:sh
dotnet run
Starting ChromeDriver 100.0.4896.60 (6a5d10861ce8de5fce22564658033b43cb7de047-refs/branch-heads/4896@{#875}) on port 55804
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.
これでChromeが起動してGoogleを開いた状態になる。
コンソールアプリを実行する時の System.Environment.OSVersion.VersionString の値は "Unix 12.3.1" となっていた。
12.3.1 という数字は、今動かしているmacOSのバージョンと一致する。
https://scrapbox.io/files/625252588a96e8001d4d2ca5.png
/icons/hr.icon
.NET MAUI で Selenium を使ってみる (失敗)
.NET MAUI の新しいプロジェクトを作るところから。
code:sh
sudo dotnet workload install maui --source https://api.nuget.org/v3/index.json
dotnet new maui -n "MyMauiApp"
cd MyMauiApp
dotnet add package Selenium.WebDriver --version 4.1.0
dotnet add package Selenium.WebDriver.ChromeDriver --version 100.0.4896.6000
生成されたサンプルコードをボタンが押されたらChromeが起動するように変更するが、これは失敗する。
MAUI + VSCode + Mac でデバッガをアタッチする方法が分からなかったので、苦肉の策として例外情報を pbcopy でクリップボードにコピーしている。
code:MainPage.xaml.cs
namespace MyMauiApp;
using System.Diagnostics;
using OpenQA.Selenium.Chrome;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
try {
var driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://www.google.com");
} catch (Exception ex) {
var startInfo = new ProcessStartInfo("pbcopy");
startInfo.RedirectStandardInput = true;
var ps = Process.Start(startInfo);
ps.StandardInput.Write(ex.ToString());
ps.StandardInput.Flush();
ps.StandardInput.Close();
CounterLabel.Text = ex.ToString();
}
}
}
実行して吐かれた例外の内容を確認する。
code:sh
dotnet build -t:Run -f net6.0-maccatalyst
OpenQA.Selenium.DriverServiceNotFoundException: The chromedriver file does not exist in the current directory or in a directory on the PATH environment variable. The driver can be downloaded at http://chromedriver.storage.googleapis.com/index.html.
at OpenQA.Selenium.DriverService.FindDriverServiceExecutable(String executableName, Uri downloadUrl)
at OpenQA.Selenium.Chrome.ChromeDriverService.CreateDefaultService()
at OpenQA.Selenium.Chrome.ChromeDriver..ctor(ChromeOptions options)
at OpenQA.Selenium.Chrome.ChromeDriver..ctor()
at MyMauiApp.MainPage.OnCounterClicked(Object sender, EventArgs e) in /Users/spinylobster/dev/dotnet/MyMauiApp/MainPage.xaml.cs:line 15
chromedriver が見つからないらしい。
この辺りは nuget で追加した Selenium.WebDriver.ChromeDriver が面倒を見てくれるはずだが、MAUIでは上手く動かない。
Windowsでも同様の問題が起きるっぽい。
chromedriver があるディレクトリを一時的に PATH に追加した上で再度実行する。
code:sh
(PATH="$PATH:pwd/bin/Debug/net6.0-maccatalyst/maccatalyst-x64"; dotnet build -t:Run -f net6.0-maccatalyst)
System.PlatformNotSupportedException: Operation is not supported on this platform.
at System.Net.Http.NSUrlSessionHandler.set_Proxy(IWebProxy value)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
--- End of stack trace from previous location ---
at System.Net.Http.HttpClientHandler.InvokeNativeHandlerMethod(String name, Object[] parameters)
at System.Net.Http.HttpClientHandler.SetProxy(IWebProxy value)
at System.Net.Http.HttpClientHandler.set_Proxy(IWebProxy value)
at OpenQA.Selenium.Remote.HttpCommandExecutor.CreateHttpClient()
at OpenQA.Selenium.Remote.HttpCommandExecutor.Execute(Command commandToExecute)
at OpenQA.Selenium.Remote.DriverServiceCommandExecutor.Execute(Command commandToExecute)
at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities desiredCapabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Chromium.ChromiumDriver..ctor(ChromiumDriverService service, ChromiumOptions options, TimeSpan commandTimeout)
at OpenQA.Selenium.Chrome.ChromeDriver..ctor(ChromeDriverService service, ChromeOptions options, TimeSpan commandTimeout)
at OpenQA.Selenium.Chrome.ChromeDriver..ctor(ChromeOptions options)
at OpenQA.Selenium.Chrome.ChromeDriver..ctor()
at MyMauiApp.MainPage.OnCounterClicked(Object sender, EventArgs e) in /Users/spinylobster/dev/dotnet/MyMauiApp/MainPage.xaml.cs:line 15
このアプリの中での System.Environment.OSVersion.VersionString を確認すると、 "Unix 15.4" となっていた。
15.4 という数字は、最新(2022年4月)のiOSのバージョン番号と一致する。
Mac Catalyst 公式ページ より
Mac Catalystを使って構築したMac向けのネイティブAppは、iPad向けAppとコードを共有でき、Mac専用の機能を追加することもできます。
ということで、どうやら普通のコンソールアプリと .NET MAUI アプリでは事情が異なるらしい。
/icons/hr.icon
スタックトレースのおかげで、 PlatformNotSupportedException の発生源は NSUrlSessionHandler.set_Proxy だということが分かった。
https://github.com/xamarin/xamarin-macios/blob/e191c613ceaca400cd1496c2bbcbe739dc97b95e/src/Foundation/NSUrlSessionHandler.cs#L582-L585
……クソやん💩
と思ってたらなんとその数日後にPRがマージされて大分マシになった。
https://github.com/xamarin/xamarin-macios/pull/14484
ただし、2022/04/10現在でも NSUrlSessionHandler.set_Proxy が呼ばれると例外が発生することは変わっていない。
https://github.com/xamarin/xamarin-macios/blob/c87e2c0c59cb42fd2cc0e769995bce4299a360a3/src/Foundation/NSUrlSessionHandler.cs#L627
Selenium はユーザがProxyを使うつもりがなくても HttpClientHandler.Proxy に null を代入するつくりになっているため、ここが変更されない限り例外は発生する。
https://github.com/SeleniumHQ/selenium/blob/a22e15736dd4f81fe0d1a7d07fe556d4ecae50ad/dotnet/src/webdriver/Remote/HttpCommandExecutor.cs#L254
「この1行をコメントアウトしたものを自分でビルドして使えば良いのでは?」と思って試行錯誤してみたが、 .NET 版 Selenium のビルドに ILMerge.exe が登場しているのを見て諦めた。 Windows 前提やんけ。
https://github.com/SeleniumHQ/selenium/blob/trunk/dotnet/private/merge_assemblies.bzl
https://github.com/SeleniumHQ/selenium/tree/trunk/third_party/dotnet/ilmerge
/icons/hr.icon
.NET MAUI で Selenium を使ってみる (成功)
HttpClientHandler.Proxy への代入を回避するもう一つの手段として、 HttpCommandExecutor とそれを使っているクラスを片っ端から自作クラスに置き換える戦法を取ることにした。
「片っ端から」と書いたが、実は ChromeDriver, ChromiumDriver, DriverServiceCommandExecutor, HttpCommandExecutor の4つだけで済むので意外と楽だった。
↓の gist の4ファイルを MyMauiApp/ 直下に作成。
https://gist.github.com/spinylobster/d3683002ef96b0ee52dc59ca385ee2c0
MainPage.xaml.cs で MyChromeDriver を使ったコードに書き換える。
ついでに Google に飛んだ後に検索も実行するようにしておく。
code:MainPage.xaml.cs
namespace MyMauiApp;
using System.Diagnostics;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
try {
var driver = new MyChromeDriver();
driver.Navigate().GoToUrl("https://www.google.com");
Thread.Sleep(1000);
driver.FindElement(By.Name("q")).SendKeys("hello");
driver.FindElement(By.CssSelector("form")).Submit();
} catch (Exception ex) {
var startInfo = new ProcessStartInfo("pbcopy");
startInfo.RedirectStandardInput = true;
var ps = Process.Start(startInfo);
ps.StandardInput.Write(ex.ToString());
ps.StandardInput.Flush();
ps.StandardInput.Close();
CounterLabel.Text = ex.ToString();
}
}
}
これで実行すれば、ボタンを押した時にちゃんと Selenium が動くようになっている。
code:sh
(PATH="$PATH:pwd/bin/Debug/net6.0-maccatalyst/maccatalyst-x64"; dotnet build -t:Run -f net6.0-maccatalyst)
成功はしたが、Selenium のバージョンが上がった時に追随しづらい点がかなり厳しいので、実際にこのやり方を採用するかは微妙なところ。
とりあえず、「.NET MAUI + macOS + Selenium は普通にやると動かないけど、頑張ればいける」ということが分かった。
.NET MAUI #Selenium