tiny hooks

Linux にも Blink Opera がリリースされて以来、結構使っている。Presto Opera と同時に立ち上げて、気分によってどちらかを使っている的な状態だ。あるいは、ナウそうなサイトは積極的に Blink Opera を選択しているかもしれない。Presto Opera では操作不能に陥るくらい重いサイトが Blink Opera ではまあまあサクサクだったりするからだ。

とはいうものの、もちろん Blink Opera より Presto Opera のほうが優れている点も少なくない。例えばキーボードまわりのカスタマイズ性だ。Presto Opera はブラウザ上だろうがアドレスバー上だろうがあらゆるキーボード入力をカスタマイズできるが、Blink Opera にそういう機能はない。

ちなみにそれほどキーバインドを変えまくっているわけではなくて、

  • [cci]c-h[/cci] でページを戻るか、戻れなければページを閉じる
  • [cci]space[/cci] でビューポートの高さの半分 だけスクロールする
  • [cci]j[/cci]、[cci]k[/cci] で 1 行ずつスクロール
  • [cci]h[/cci]、[cci]l[/cci] でタブを切り替え
  • [cci]c-b[/cci]、[cci]c-f[/cci]、[cci]c-n[/cci]、[cci]c-p[/cci] あたりを textarea に対して定義
  • [cci]c-b[/cci]、[cci]c-f[/cci]、[cci]c-n[/cci]、[cci]c-p[/cci] あたりをアドレスバーに対して定義
  • textarea 上での [cci]c-h[/cci] をカーソル前の1文字削除にする

この程度である。かわいいものである。これをなんとかして Blink Opera に持ってきたいのだけど、どうすればいいだろうか。

まず考えられるのは、そういうキーボードコンフィグ系のエクステンションがすでにあるよね、なくても Chrome 版をむりやり動かせばいいよね、ということだ。しかし、問題は、[cci]c-h[/cci] なのである。これの場合、単にキーに機能を割り当てるのではなく、条件判断が必要になる。そういうことを許してくれるエクステンションはあるだろうか? たとえば Keyconfig は許可してくれるだろうか?

しかしどうやら Keyconfig はそういうことはできないようだ(できるのならごめんなさい)。YakShave ならできるかもしれないが、何やらローカルに web サーバを立てる必要があったりとなんだかめんどうそう(ごめんなさい)。

というわけで、もしかして、wasavi に組み込んだほうが早いんじゃねーの!? という気分になりつつある。

play a sound #3

sounds

wasavi の起動時に効果音を出すようにしてみた。

そうすると、当然「そんなのいらねーです!」というリクエストが来ることが考えられる。したがって音を出すかどうかを設定できるとうれしい。

ところで起動時の効果音というのは、wasavi が起動する前に鳴らすわけなので、つまり wasavi 本体が管理する設定([cci]:set[/cci] で制御できるもの)とは別になる。そこで、オプションページで設定することになる。オプションページに効果音のリストとそれを鳴らすかどうかのチェックボックス、そしてボリュームの設定を置いた。

ところで従来は、wasavi が出す音といえばエラー時のビープ音であった。それは [cci]bellvolume[/cci] で設定できたのだけど、ビープ音も前述の仕組みで鳴らすようにしたので bellvolume は obsoleted になった。

play a sound #2

SDK のドキュメントをつらつらを眺めてみると、HiddenFrame なるものを見つけた。これは要するに、バックエンド側で保持することのできる iframe のようなものらしい。HiddenFrame 内に構築される window はごくごく普通のそれであり、Audio コンストラクタも持っている。

そういうわけで、Firefox においては、HiddenFrame 経由で Audio 要素を得ることができた。子供の window で生成したオブジェクトをそのまま親の側で使用できてしまうのがかなり気持ち悪いが、たぶん、きっと、だいじょうぶ。

play a sound

wasavi を実行中、音を出す場合がある。つまりビービー鳴らすモードを持つ vi と同様、必要なときにエラーベルを出す。もし「この音うるせーよ!!」と思われる場合は、ワークアラウンドとして [cci]:set bellvolume=0[/cci] とすれば、とりあえずミュート状態にはなる(例えばこの方がやっている方法だ)。将来的にはビジュアルベルに切り替わったりする気遣いをするかもしれない。

さてこの時に出す音は Audio 要素を使っていて、フロントエンド側で鳴らしている。音源は wasavi 起動時にバックエンドから送られてくる ogg または mp3 データである。

しかし、これは割と無駄なのではないか。複数のタブで wasavi を動かしているとして、各タブがもともと同じデータである音源のコピーを個別に抱えている状態だ。現状は 1 秒未満しか発音しないビープ音なのでデータ自体は数 KB だ。したがってものすごく容量を食っているというわけではないが、やっぱり無駄だ。

これを、Audio 要素をバックエンドで保持するようにして、フロントエンドで音が必要になったら発音リクエストを投げるだけにすれば、そういう無駄は解消できる。ということでやってみて、Opera と Chrome では思ったとおりになったのだけど、Firefox が問題だ。Firefox の Add on SDK 上のバックエンドは、Window ではない不可思議なサンドボックスオブジェクトがグローバルになっていて、つまり Audio 要素をバックエンド側で使えないのだった。

こういう場合、たいてい [cci]Cc[/cci] や [cci]Ci[/cci] といった Firefox 特有の黒魔術をごにょごにょして何とかなったりするものなのだが(Kosian では XHR や Blob や FormData についてごにょごにょしている)、黒魔術はしょせん黒魔術であり、あんまり使いたくない。なにしろ、AMO にアップする際の機械的チェックにすら「おい黒魔術使ってんぞ、気をつけろよな!」などと言われる始末である。禁止されているわけではなく、「気をつけろ」なのがミソではあるのだが。

さて、サウンドプレイヤについてもこの黒魔術を通して Audio 要素っぽいものを得ることはできるようなのだけど

var {Cc, Ci} = require("chrome");
var sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
var uri = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService)
.newURI(self.data.url(...), null, null);

sound.play(uri);

これで得られる nsISound は Audio 要素とは全然異なるものであって、たとえばボリュームの指定とかはできないようなのである。

Positioning

文書のスクロール量によりtextareaが半分隠れている場合などを考慮して初期の位置決めにおけるクリッピングを真面目にやるようにした。これにより gmail などで wasavi 自身がビューからはみ出すことがなくなった。

ということでスクリーンショットでも貼ろうかと思ったのだけど、なかなか gmail のページはおいそれとお外に出せないプライベートな情報満載なのであった。

ところで、最近 github に上がった issue はほぼ対応でき、ぼちぼちエクステンションストア上の安定版(ということになっている) wasavi も更新する時期に来ている。ただ設定ページでショートカットキー書くのたるいよー! 入力したキーストロークを自動的にショートカットキー記述にしてよー! 的なアレが残っていて、これは安定版にはタイミング的に入らないかもしれない。なぜこれを残していたのかといえば、設定ページ自身のスクリプトの構成も変えたので、そのへんが固まってからにしようということだったのである。

kosian is here #7

wasavi を構成するコンポーネントとして

  • バックエンド
  • フロントエンド: エージェント
  • フロントエンド: wasavi 本体

の 3 種がある。バックエンドはいわゆるエクステンションのバックグラウンドスクリプト、フロントエンドはコンテントスクリプトなどと言われるものである。エージェントは textarea 要素を持つページにアタッチされて、wasavi を起動させるかどうかの監視などを行う。

これら 3 つが協調することで wasavi が動作する。協調するためにはメッセージングを行う必要がある。

フロントエンドとバックエンド同士のメッセージングは、各ブラウザのエクステンションがその仕組みを提供しているので問題はない。一方で不思議なことに、どのエクステンションも、ドキュメント同士のメッセージング機構はない。

おそらく、ドキュメント同士なら html の規格通りにクロスサイトメッセージングを使ってね! ということなのだろう。でも、[cci]window.postMessage()[/cci] によるやりとりというのは非常に汎用的なものなので、つまりだれでもリスンし放題なのである。怪しげなサイト上で wasavi を起動したりした場合、エージェントに対してクリティカルな情報(たとえば、wasavi のレジスタの内容は更新されるたび同期のためエージェントに内容が通知される)を直接投げるのはちょっとこわい。

ということで、ドキュメント同士のメッセージングとしてはバックエンドを経由してエクステンションのシステムの中で完結するようにした。正確には、もともとそうなっていたのだけど、ここ数ヶ月のソースは [cci]window.postMessage()[/cci] を使うようになっていた。それを元に戻した。

そして、そろそろ各ブラウザのエクステンションストアに置いてある版もここのところのテストが終わったら更新する頃合いだと思う。

kosian is here #6

wasavi のフロントエンドとバックエンドとの通信の方式について若干の変更を行いたい。

そのあたりはもちろん kosian 側で抽象化してあるのだが、wasavi はまず Chrome 上で作り始めたので、Chrome のエクステンションにおけるバックグラウンドとコンテントスクリプトの通信の作法をだいたいそのまま持ってきている。

つまり
extension.postMessage(message [, callback])
というメソッドを用いる。この callback がこの記事の主役である。

この callback を指定することにより、投げたメッセージに対する直接の応答を得ることができる。つまり非常に限定されたメッセージリスナとみなすことができる。一方、フロントエンドはコンテントスクリプト毎の汎用メッセージもリスンしていて:
chrome.extension.onRequest.addListener(handleMessage);
こちらは例えば他のタブで動作中の wasavi で設定が変更されたという同期通知や、dropbox(など)からのファイル読み込みの進捗通知などを扱う。

要するに、2 種類のメッセージリスナを扱っている。この状況で、それぞれが扱うメッセージの性格が完全に分離されているのならば、別に 2 種類あることは悪いことではないのだけど。がしかし上記のファイル読み込みの進捗通知というものが、微妙にどっちつかずな存在なのである。

ファイル読み込みは、フロントエンドからバックエンドへパスなどを渡し、バックエンドで認証などを済まし、読み込むためのリクエストを発行し、その結果をフロントエンドへ返す。フロントエンド側で見るとこの処理は ex コマンド [cci]read[/cci] の処理ということになり、そのハンドラの中で完結するはずのものである。が、実際は [cci]read[/cci] コマンド実行中のバックエンドからのメッセージは、汎用メッセージ側で処理している。これはやや不格好だ。

なぜ postMessage() の callback を使わないのかといえば、つまり [cci]read[/cci] 実行中、バックエンドからのメッセージは複数回送信されるからである。callback は 1 度呼び出されて終わりなのだ。これを拡張し、複数回 callback が呼び出される構造にすれば、必要な処理はすべて [cci]read[/cci] ハンドラの中に掛けば済むようになって、収まりがよいはずだ。

というわけでそうした。ここで、ソースの修正は割とすぐに終わったのだけど、実際に Opera 12 で動かしてみるとさっぱり狙った動作をせず、あーでもないこーでもないとした挙句、単に Opera を再起動させたらちゃんと動くようになったというなんとも言えない事件があったのだが、まあ Opera においては特に珍しいことではないので、いいのだ。

kosian is here #5

kosian ベースの wasavi の作成がひと通り作り終わったので、Selenium によるテストをしている。Chrome、Opera、Firefox のそれぞれでテストをしているのだが、

  • Chrome: 安定して自動テストを行うことができる。また、速い
  • Opera: 不安定な時期もあったが、最終バージョンの operadriver においては安定している。遅くはない
  • Firefox: キーボード入力のエミュレーションがろくに動かない。wasavi のテストでこれは致命的だ。また、べらぼうに遅い。Chrome/Opera での全テストがだいたい 80 分ほどで完走するのに対し、2 〜 3 倍ほどの時間がかかる。それから、必ずフォアグラウンドの状態にしておかないとまともに動かない

と、こんな調子である。Firefox だけが根本的にダメダメなのが気になる。なんとなく環境依存の現象がしないでもないがそんなほいほいテストマシン用意できないしなあ…。

kosian is here #4

  • Firefox でもだいたい動くようにした
  • エクステンションのビルドは make で行うようにした

ということで、Ant は使わなくなったのだが、機能テストは例によって Selenium を用いるので、Ant というか java は結局使うことになる。

kosian is here #3

kosian ベースの wasavi を Firefox で動かすべくあちこち直している。

Chrome と Presto Opera のエクステンションの仕組みは、もちろん同じではないにしても、いとこ程度には基本的な構造は似通っているので、一方が動けば他方はそれに合わせて直すだけでいいので楽なのだ。

問題は Firefox の Add-on SDK なのである。このプラットフォームで作る拡張機能というのは、かなり異質だ。Chrome や Opera の遠い親戚の、隣に住んでいるバングラデシュ人くらい異質だ。

Add-on SDK でいわゆる content script 的なものを動かすには、PageMod API を使う。このとき、どの URL で content script をアタッチし、あるいはどの URL ならアタッチしないかという include と exclude の情報が必要になる。wasavi の場合フロントエンドは agent と wasavi 本体にコードがわかれていてアタッチの仕方が若干複雑なので、exclude 機能は必須である。で、どちらを指定する機能も、Chrome と Opera は当然持っている。

Add-on SDK の PageMod は include の指定しかできないのである。意味が、わからない。そして、最新のドキュメントを見ると exclude 機能は Firefox 32 で実装されるよ! 乞うご期待! という扱いになっているのである。意味がわからない。

どうするか。include には URL がマッチすべき文字列か正規表現オブジェクトを渡すことになっている。ここで PageMod が求める正規表現オブジェクトとは必ずしも RegExp のインスタンスである必要はなく、test() と exec() メソッドを備えていてそれぞれがそれぞれの働きをすれば通してくれるので、今まで wasavi ではそのように振る舞う独自のオブジェクトを渡していた。この時点でちょっとアクロバティックである。

で、Firefox 30 でそのコードを動かしてみると見事に動かない。なんと include に渡されたものが文字列か RegExp インスタンスかどうかのチェックを行うようになったのである。このチェック自体は正しい。しかし前述の通り Firefox 30 の PageMod には exclude 機能はない。意味がわからない。どうしろというのか。

仕方がないのでさらにアクロバティックなことをして(互換性はないのに constructor を RegExp にする)動かすようにしたのだが、もちろんかなり嫌なコードである。なお、さらに PageMod 内部で摩訶不思議な RegExp インスタンスのキャッシュ機構が働いているらしく、toString() も上書きしないとダメだったりする。本当にがっかりさせられる。

最初から exclude 機能をつけてくれればこんな変なことをする必要はないのだし、そもそも include 機能があって exclude はないという API のデザインセンスの意味がわからない。

こういう記録が残るブログであまり穏やかではないことは書きたくないのだけど、Add-on SDK チームってバカなんかじゃないかと思う。