yunomuのブログ

酒とゲームと上から目線

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はあんまし使わないかなぁ。

しかしストリームをイベントに変換してそれをまたストリームに変換とういのもなんか妙な話ですね。