シェルスクリプトからwebsocat(WebSocket)でChrome DevToolsに接続しブラウザを自動操作する

## はじめに

この間「[シェルスクリプト用の WebDriver bindings](https://qiita.com/ko1nksm/items/1b7b89ce9ccc677e419e)」を作ったのですが、どうやら世の中は更に進んでおり、Chrome Dev Protocol (CDP) 経由でより高機能な Chrome DevTools の機能による自動操作が使われてきているということを知りました。WebDriver は W3C で標準化されてるので多くのブラウザで使えるというメリットがあるとは言え、機能やパフォーマンスは DevTools の方が優れており、Chrome 以外のブラウザにも搭載されつつあるので対応させたいところです。しかし CDP では HTTP ではなく WebSocket を使います。はたしてそれがシェルスクリプトから呼び出せるのか？ということでやってみました。

## websocat

WebSocket は ハンドシェイクにこそ HTTP と同じ形式を使用しますが HTTP ベースのプロトコルではありません。HTTP であれば `curl` や `wget` コマンドが使えそうですが CDP は WebSocket なので使うことができません。そもそもシェルスクリプトは POSIX 準拠の範囲ではネットワークに対応していません。bash, ksh であれば /dev/tcp から、zsh であれば net/tcp モジュールを使って TCP プロトコルを使うことが可能ですが WebSocket に対応させるのは大変ですし、どちらにしろ使えるシェルが限られます。

そこでいろいろと探してみるとどうやら [websocat](https://github.com/vi/websocat) を使えば WebSocket 通信が出来きそうだということがわかりました。そして都合のいいことに CDP 呼び出しのサンプルまで書いてありました。（読みやすいように整形しています。JSONは実際は一行です。）

```console
$ chromium --remote-debugging-port=9222 &

$ curl -sg http://127.0.0.1:9222/json/new \
  | grep webSocketDebuggerUrl | cut -d'"' -f4 | head -1
ws://127.0.0.1:9222/devtools/page/A331E56CCB8615EB4FCB720425A82259

$ echo 'Page.navigate {"url":"https://example.com"}' | websocat -n1 --jsonrpc \ 
  ws://127.0.0.1:9222/devtools/page/A331E56CCB8615EB4FCB720425A82259
{
  "id":2,
  "result":{
    "frameId":"A331E56CCB8615EB4FCB720425A82259",
    "loaderId":"EF5AAD19F2F8BB27FAF55F94FFB27DF9"
  }
}
```

この通りにやれば動くであろうということでやってみたのですが以下のようなエラーがでて動きませんでした。

```json
{
  "error":{
    "code":-32600,
    "message":"Message has property other than 'id', 'method', 'sessionId', 'params'"
  }
}
```

問題を解決する鍵は `'Page.navigate {"url":"https://example.com"}'` と `--jsonrpc` です。前者は `websocat` が対応してる特殊なフォーマットで `--jsonrpc` オプションによって以下のような JSON-RPC 2.0 形式に変換されます。

```json
{
  "jsonrpc": "2.0",
  "method": "Page.navigate",
  "params": {"url":"https://example.com"},
  "id": 1
}
```

エラーメッセージから `jsonrpc` キーが余計なのでは？と思い、以下のように `jsonrpc` を削って JSON で直接送信してみると動作しました。

```sh
echo '{"method":"Page.navigate","params":{"url":"https://example.com"},"id":1}' \
  | websocat -n1 ws://... 
```

しかし `params` キーにオブジェクトが使えるのは、[ここによると](https://stackoverflow.com/questions/24404108/json-rpc-parameters-version-1-0-vs-2-0) JSON-RPC 2.0 からのようなのでフォーマット自体は 2.0 で間違いなさそうです。おそらく CDP 側で余計なバリデーションをしてるバグがあるような気がします。


## Selenium/WebDriver bindings for shell script との統合

さて私がやりたいのは Selenium/WebDriver bindings for shell script との統合です。とりあえず簡易的に組み込んでみました。

```sh
#!/bin/sh

set -eu

. ./lib/webdriver.sh

chrome_options() {
  echo '{ "args": [] }'
  # echo '{ "args": ["--headless"] }'
}

WebDriver driver="$(ChromeDriver chrome_options "http://localhost:9515")"
debugger=$(driver capabilities | jq -r '.["goog:chromeOptions"].debuggerAddress')
wsdebugger=$(curl -sg "http://$debugger/json/new" | jq -r .webSocketDebuggerUrl)
json='{"method": "Page.navigate", "params": {"url":"https://example.com"}, "id": 1}'
echo "$json" | websocat -n1 "$wsdebugger"

sleep 3
driver quit
unset -f element driver
```

接続先アドレスを WebDriver から取得しているだけでやっていることは大して変わりません。動作させることは出来たのでこれをわかりやすいインターフェースに変更するすれば終わりです。例えばこんな感じにすると良さそうです。

```sh
WebDriver driver="$(ChromeDriver chrome_options "http://localhost:9515")"
driver Page.navigate '{"url":"https://example.com"}'
```

あとは双方向通信が必要な場合はどうするんだ？という問題があるのですが、たぶん多くは双方向通信は必要ないと思うので大部分の CDP の機能は使えるんじゃないかと思います。WebSocket から知らなかったので調べるのに時間がかかりましたが、`websocat` のおかげで思ったより簡単に DevTools の機能が使えそうです。なお CDP でどんな機能が使えるかは https://chromedevtools.github.io/devtools-protocol/ を参照してください。