Humanity

Edit the world by your favorite way

Vim script で Chrome Debugging Protocol を扱うライブラリを作りたい

github.com

タイトルの通りなのですが、Chrome Debugging Protocol (以下 CDP) を扱うためには、

  1. CDP は WebSocket ベースなので WebSocket を実装しなきゃならない
  2. バイト列をパースする際にヌルバイトを Vim では扱えない

となって悩んでいます。 ヌルバイトを扱う部分は Python/Ruby/Lua インターフェースを使ったり、外部コマンドでやったりと策がないわけではないのですが、 どうせなら Vim で扱う方法を考えてみようというわけです。

CDP を実装しようとしたきっかけ

Chrome を (CDP が許す限り) 意のままに操ることができるからです。 まだ CDP をざっくりとしか調べてないのであまり風呂敷を広げすぎるのもアレかと思うのですが、 大体こんな事ができるんじゃないかと思っています。 できるか分からない部分に関して一応 (多分) とか付けときます。

  • (多分) Chrome を画像ビューアとして使ったり?
  • (多分) ライブリロード的なことができたり?
  • なんと JS のデバッグもできる (本来の機能)

正直今回この記事を書こうと思ったモチベーションはこんな事ができるんだよっていう紹介ではなくて、 バイト列をパースする際にヌルバイトを扱えない問題に対する提案がメインです。 あと単純に自分が何に悩んでいるかを整理したいというのもあります。

WebSocket の実装

対象の Chrome は通常 localhost で動いていて、over SSL でもないので Vim 8 の channel で扱えます。 channel は ch_open() でソケット通信したり、job_start() で実行したコマンドの入出力が channel で扱えます。 ようはざっくり入出力を抽象化したオブジェクト (ハンドル) と考えればいいです。

最初のハンドシェーク部分に関してはまんま HTTP なので実装できましたが、 バイト列 (=ヌルバイトを含む文字列) を Vim で扱う際にできれば (if_python などの外部インターフェースを使わず) pure vim script で実装したい… そうなると今の channel の機能では扱えないことが分かりました (次の章で解説)。

ちなみに、先ほど channel を入出力を抽象化したオブジェクト (ハンドル) と言いましたが、そう言ってしまうにはまだ機能が少なくて、 例えばファイル/バッファも channel で扱えるようになって ch_read() 等で固定長読み込む事ができるようになれば、getline() (行単位で文字列を取得) だけじゃなく もっと自由度と抽象度の高いスクリプトが書けるようになると思います。

バイト列 (=ヌルバイトを含む文字列) を Vim で扱う

閑話休題

まず、Vim script の文字列ではヌルバイトを含む文字列を扱う事ができません。 扱えないといっても、 echo "hell\0o"hell になる (多言語の人から見たら信じられない仕様だと思いますw) ように Vim script の文字列の値として使えないだけで、バッファ上で扱うことはできます。 ヌルバイトを含んだバッファで :%!xxd とすると現在のバッファの内容が hex dump した内容に置き換わりますが、 そうするとヌルバイトもちゃんと xxd コマンドに渡されていることが分かります。 ちなみに xxd コマンドは Vim に標準添付されているので、一度バッファ上に内容を持ってしまえば job 機能から xxd を呼び出して hex dump した内容を持てば Vim script で扱うことが可能です。 つまり目標としては「WebSocket で受け取ったバイト列をバッファに読み込む」です。

しかし、調べてみると ch_open('localhost:12345') のように開いたチャンネル (ソケット) からは受け取り方法としてバッファを指定できないことが分かりました。 job 機能だったら job_start() の option に {'out_io': 'buffer', 'out_buf': bufnr} のようにバッファを指定できます。 しかし channel にはそれが無いのです。

しかし、さらに調べる内にある方法でできなくはないことが分かりました。 できなくはないけどがかなり辛いハックになりそうだということも分かりました (つらい)。

その方法とは、ch_logfile({fname} [, {mode}]) で書き込んだログファイルに現在 Vim で使用している channel が受け取ったメッセージの内容が書き込まれていたので、これをパースすればできなくはないということです。 しかしこの関数はそもそも引数から分かる通り、ロギングしたい channel を渡す設計ではなく Vim で使用している全 channel のログが {fname} にすべて書き込まれます。 なんでこんな設計になっているかというと、まぁ恐らくテスト用 (あるいはデバッグ用) に追加した関数だからだと思います。 それに現在ロギングしているかどうかも Vim script で判断できないので、この関数を使うと既存でロギングされていたファイルへは書き込まれなくなることが予想されます。

そんな感じで ch_logfile() はそもそもそんな用途に使うもんじゃねーよ感がビンビンですが、 ただ各行に channel の id と一緒に吐き出されるのでパースするのは不可能じゃなさそうです。 この channel id は ch_info() で取得することができます。 ただ複数行のメッセージが来た場合も考えて単純に各行取得したんじゃパースできないため、ちゃんとやるのも結構大変な印象です。

本来なら先ほど言ったように ch_open() の option でも job option みたいに {'out_io': 'buffer', 'out_buf': bufnr} みたいに渡せると バッファ経由でヌルバイト含むバイト列も扱えそうなのですが、何とかならないでしょうか…

[追記] CDP の実装途中のスクリプト

一応共有しておきます。

WIP: [Preview] Chrome Debugging Protocol in Vim script · GitHub

今回の記事関係ないけど channel 使った HTTP の実装は vital.vim に入れたい… ただ channel では https:// な URL にリクエスト送れないので、その場合は別の実装に fallback したり、 あと Promise で無理矢理書いてる所を Observable のライブラリ作ってそれで置き換えるとかやりたいです。 Vital.Async.Observable がほしいって話は前から言ってますが一向にやる気配がありませんね…?(ウッ)

あとバイト列を扱う Vital.Data.Blob もほしい。 ほしい物がありすぎてパンク気味です…(実装力不足が悩ましい…)