Chrome remote-debuggingの話と見せかけてWebSocketとかfrappuccinoとか
最近ちょっとブラウザで遊んでいたので、共有というか。
Google Chromeにremote debuggingという機能があります。
https://developers.google.com/chrome-developer-tools/docs/debugger-protocol
要するにJavaScript開発者にはおなじみのDeveloper toolsが使っているAPIなんですが、最近モバイル対応だのなんだののために整備されて使いやすくなったとかなんとか。最近て、何年前の話か知りませんけども。
使うためには、remote debugging portを有効化してChromeのプロセスを起動する必要があります。Chromeのプログラム名は環境によって違うので、それらしいやつを起動してください。
% google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp
とやると起動します。9222はなんか、デフォルトのポートらしいです。
user-data-dirはプロファイルのディレクトリで、指定しないと既存のChromeプロセスがforkするだけでremote-debuggingは有効になりません。既存のプロセスが無ければうまくいくんですが。
起動すると、
http://localhost:9222/
にアクセスできるようになります。このトップページは現在開いているタブの一覧です。それぞれのページを開くと、おなじみのdeveloper toolsが開きます。developer toolsってJavaScriptで書かれてたのかよという。
http://localhost:9222/json
にアクセスすると、開いているタブの情報がJSONで取れます。
この中にある”webSocketDebuggerUrl”というやつが、remote-debugging APIのエンドポイントです。その名の通り、WebSocketで通信します。
ここからは手打ちでは辛いのでRubyで操作します。
まず、どうでもいいんですがwebSocketDebbuggerUrlの取得部分。
require 'json' require 'open-uri' # ページ一覧のJSONを取ってくる def pagelist(host = "localhost", port = 9222) JSON.parse open("http://#{host}:#{port}/json") {|f| f.read } end # とってきたやつからタイトルに”GitHub”が含まれる最初のものを探す w = pagelist.find {|e| e["title"].include? “GitHub } # webSocketDebuggerUrlを取得 debugUrl = w["webSocketDebuggerUrl"] || begin puts "debugUrl is nil" exit 1 end
ここではタイトルに”GitHub”を含む最初のタブのdebugger urlを取得していいます。これとは別にdeveloper toolsを使っていたりするとこのインタフェースが無くなったりするので、念のためにエラー処理を入れてます。
で、WebSocketでの通信ではwebsocket-client-simpleを使います。たぶんこれが一番楽です。
% gem install websocket-client-simple
# (さっきの続き) require 'websocket-client-simple' # 接続 ws = WebSocket::Client::Simple.connect debugUrl # 以下はイベントハンドラ ws.on :message do |msg| data = JSON.parse(msg.data) print "response: ", data["params"], $/ end ws.on :open do puts "open" end ws.on :close do |s| puts "close" end
基本はこんな感じ。イベントドリブンになっていて、message, open, closeというイベントが起きます。messageは受信した時、openは接続完了した時、closeは閉じた時。
それとは別に、sendというメソッドがあります。
試しにTimelineで流れてくるイベントを取ってみます。
https://developers.google.com/chrome-developer-tools/docs/protocol/1.1/timeline
ここに載ってるstartというメッセージを送ると、ブラウザ側からイベントのログがドロドロと流れてきます。
WebSocketのopenのタイミングが微妙なのでopenハンドラに直に送信処理を書きます。
ws.on :open do ws.send JSON.dump("id" => 1, "method" => "Timeline.start") end sleep 5
こうすると5秒ほどイベントが流れてきます。流れてこないという人はJavaScriptイベントが起きていない可能性があるのでブラウザのページ上でマウスを動かしたりなんたりしてみてください。
あと、このidというのは、まあなんかidらしいです。適当に決めてます。
{ "record"=>{ "startTime"=>1388377232581.642, "type"=>"Program", "data"=>{}, "children"=>[], "endTime"=>1388377232581.6663 } }
イベントの1個の形式がこんな感じで(Rubyに整形済み)、いわゆるTimelineの情報が入っているんですが、このstartTime/endTimeというやつが、エポックからのまさかのマイクロ秒で、ミリ秒じゃねーのかよってつまんないところでつまづきました。
frappuccino
messageイベントのハンドラを書くのが面倒くさい。ので、frappuccinoでイベントをストリームに変換してみようと思いました。いわゆるFRPライブラリです。
https://github.com/steveklabnik/frappuccino
% gem install frappuccino
require 'frappuccino' # イベントストリームを作るやつ class Emitter def send(data) emit(data) end end emitter = Emitter.new stream = Frappuccino::Stream.new(emitter) # debug: 送られてきたイベントを画面に出力する #stream.on_value do |v| # puts v #end # eventRecordedだったら画面に出力する stream .select {|d| d["method"] == "Timeline.eventRecorded"} .on_value {|d| puts d } # eventRecordedじゃなかったら"log"というファイルに書き出す stream .select {|d| d["method"] != "Timeline.eventRecorded"} .on_value {|d| open("log", "a+") {|f| f.puts d } } # (中略: 最初に書いたWebSocketを接続したりする処理) # データを受け取ったらストリームに流す ws.on :message do |msg| emitter.send JSON.parse(msg.data) end
これはわかりやすくなったんだろうか?
でもまあ、今回みたいにselectが複数あったりとか、あと畳み込みをしたくなった時には結構いいかもしれません。この例だとzipやmergeはあんまし使わないかなぁ。
しかしストリームをイベントに変換してそれをまたストリームに変換とういのもなんか妙な話ですね。