Hatena::ブログ(Diary)

箱入りあじゃらの日記

 | 

2011-12-06

Google ChromeをWebSocketで制御する

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の6日目。初参加です、緊張します。

WebSocketネタということで、ChromeWebKit Remote Debugging Protocolを使ってWebSocketでGoogle Chromeを操作する小ネタ。

手順

1.リモートデバッグを有効にしてChromeを起動

Chromeの起動オプションに--remmote-debugging-portを付加してリモートデバッグを有効にする。

$ chrome --remote-debugging-port=9222

2.タブ一覧を取得

今回はコントロールする側(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"
} ]

意味はそのまんまです。


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}

f:id:ajalabox:20111128222008p:image

動作するようです。

次に、折角なのでidを使ってcallback風に処理できるようにしてみました。 (12/9 ソースコード一部修正)

chrome.js

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について

またこの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からも離れている気がしますけどこまけぇことは気にすんな、です。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/ajalabox/20111206/1323151303
 | 
最近のコメント
プロフィール

ajalabox

ajalabox

●w● http://twitter.com/#!/ajalabox