JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の6日目。初参加です、緊張します。
WebSocketネタということで、ChromeのWebKit Remote Debugging Protocolを使ってWebSocketでGoogle Chromeを操作する小ネタ。
Chromeの起動オプションに--remmote-debugging-portを付加してリモートデバッグを有効にする。
$ chrome --remote-debugging-port=9222
今回はコントロールする側(Chrome)とされる側(node)が同一ホストで動いてるものとします。
http://localhost:9222/jsonをGETで叩くとChromeのタブ一覧が得られます。
叩いてみた結果の例:
[ { "devtoolsFrontendUrl": "/devtools/devtools.html?host=localhost:9222&page=1", "faviconUrl": "http://twitter.com/phoenix/favicon.ico", "thumbnailUrl": "/thumb/http://twitter.com/", "title": "Twitter", "url": "http://twitter.com/", "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/1" }, { "devtoolsFrontendUrl": "/devtools/devtools.html?host=localhost:9222&page=3", "faviconUrl": "http://www.google.co.jp/favicon.ico", "thumbnailUrl": "/thumb/http://www.google.co.jp/", "title": "Google", "url": "http://www.google.co.jp/", "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/3" } ]
意味はそのまんまです。
接続したいタブを選んで、webSocketDebuggerUrlを使って接続しJSONフォーマットのコマンドを発行。使えるコマンドの一覧と書き方はhttp://code.google.com/intl/ja/chrome/devtools/docs/protocol/0.1/index.htmlに載っています。
コマンドはid, method, paramに分かれていて、methodにコマンド名、paramにオプションを書いて送信します。指定されたidはコマンドへのChromeからのレスポンスにつけられるから、これを使ってコマンドの実行結果などを管理することができます。
Chromeと通信してプロトコルのRuntime.evaluateを使って、ページタイトルを変更するコード。
WebSocket通信にwebsocket-clientモジュールを使っています。
var http = require('http'); var WebSocket = require('websocket-client').WebSocket; var PORT = 9222; var HOST = 'localhost'; //Chromeの開いているタブの一覧を取得する function getOpenTabList(host, port, callback) { var http_options = { host: host, port: port, path: '/json' }; http.get(http_options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { callback(JSON.parse(chunk)); }); }).on('error', function (e) { throw e; }); } getOpenTabList(HOST, PORT, function (list) { var wsUrl, ws; //とりあえず開いていて接続できるタブを使う for (var i = 0, l = list.length; i < l; i++) { if (list[i].webSocketDebuggerUrl) { wsUrl = list[i].webSocketDebuggerUrl; break; } } if (!wsUrl) { return; } ws = new WebSocket(wsUrl); ws.on('open', function () { //ChromeとのWebSocket通信開始 //ここで始めてChromeを制御できる //Chromeにコマンドを送信 ws.send(JSON.stringify({ "id": 1, "method": "Runtime.evaluate", "params": { "expression": "document.title=document.title + '[Remote]'" } })); }); ws.onmessage = function (buf) { //実行結果を受け取る console.log(buf.data); }; });
実行結果:
{"result":{"result":{"type":"string","description":"Twitter[Remote]"}},"id":1}
動作するようです。
次に、折角なのでidを使ってcallback風に処理できるようにしてみました。 (12/9 ソースコード一部修正)
var http = require('http'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var WebSocket = require('websocket-client').WebSocket; function Chrome(host, port) { this.host = host; this.port = port; this.ws = null; this.onconnect = null; this.queue = []; this.sending = -1; this.callback = null; this.n = 0; this.on('wsconnect', onWSConnect); this.connect(); } util.inherits(Chrome, EventEmitter); function onWSConnect() { this._send(); }; function onWSOpen() { this._send(); this.emit('wsconnect'); }; function onWSMessage(data) { this._receive(JSON.parse(data.data)); this.emit('message', data); }; Chrome.prototype.connect = function () { var http_options = { host: this.host, port: this.port, path: '/json' }; var that = this; http.get(http_options, function (res) { res.on('data', function (chunk) { that.tabList = JSON.parse(chunk); that.emit('connect', that.tabList); }); }); return this; }; Chrome.prototype.wsConnect = function (tabId) { var ws; if (!this.tabList) { throw new Error('yet to be connected'); } if (!this.tabList || !('webSocketDebuggerUrl' in this.tabList[tabId])) { throw new Error('invalid tab id'); } ws = this.ws = new WebSocket(this.tabList[tabId].webSocketDebuggerUrl); ws.on('open', onWSOpen.bind(this)); ws.onmessage = onWSMessage.bind(this); return this; }; Chrome.prototype.send = function (method, params, callback) { this.queue.push({ data: { id: this.n++, method: method, params: params || {} }, callback: callback }); this._send(); return this; }; Chrome.prototype._send = function () { var task; if (!this.ws || this.queue.length <= 0 || this.sending !== -1) { return this; } task = this.queue.shift(); this.ws.send(JSON.stringify(task.data)); this.sending = task.data.id; this.callback = task.callback; }; Chrome.prototype._receive = function (data) { if (this.sending !== data.id) { return; } if (this.callback) { this.callback(data.result); } this.sending = -1; this._send(); }; Chrome.prototype.close = function () { this.ws.close(); }; module.exports = Chrome;
若干callbackが重なっているのが気になりますけど。
Twitter Webを開いているタブにアクセスして、ツイートの含まれるノードを削除します。
またこのRemote Debugging Protocolは本来その名の通りWebkitをリモートからデバッグするためのもので、Webkit Inspectorのリモートデバッグ機能でこれが使われています。
手順1のようにChromeをリモートデバッグ可能な状態にして起動して、別のChromeからhttp://localhost:9222にアクセスするとタブのサムネイルが表示されるのでそれをクリックしたらInspectorを起動してリモートデバッグできます。
ただしこの時同一PCで実験するときは、必ず制御する方とされる方のChromeを別のプロファイルにしないといけません。デバッグされる方のChromeにさらに--user-data-dirオプションをつけて起動すれば大丈夫です。
$ chrome --remote-debugging-port=9222 --user-data-dir=remotetest
ちなみにリモートデバッグしているInspectorを右クリック→[要素の検証]を選択してさらにInspectorを立ち上げた後、それのコンソールで
window.dumpInspectorProtocolMessages = true
を実行すると通信内容をダンプすることができます。眺めていると楽しいです。
WebSocketで制御できるということを知って衝動的に書いてしまいました。
ちょっとWebSocketからもNodeからも離れている気がしますけどこまけぇことは気にすんな、です。