Talk about news

PhoneticNews という Chrome エクステンションを前書いたのだが、いくつか既知のバグがあり、特に Chrome の起動時に二重に読み上げが行われる現象がある。この機会に探ってみた。

このエクステンションは定期的に NHK のニュースを読み込み、それを読み上げる。そのためにバックグラウンドで定期的に読み込み処理、読み上げ処理を走らせることが動作の核になる。一般的にそれを実現するには setTimeout や setInterval を使うのだが、Chrome のエクステンションの場合はバックグラウンドページの構造について Event Pages という別のアプローチがあり、PhoneticNews もそれを使っている。しかしこれがなかなかに癖があり、正しくそれに対応していなかったのがバグの原因になっている。

エクステンションのバックグラウンドが Event Pages のルールに従うか否かで変わる最大の物は、つまりバックグラウンドページが勝手にアンロードされるか否かということだ。バックグラウンドがアイドル状態になると即アンロードされる。アンロードというのは Chrome ブラウザの環境下においては、エクステンションに割り当てられたプロセスを終了するという意味だ。つまり Event Pages に対応すると、タスクマネージャを開いた時ずらっと Chrome が並ぶのを幾ばくか減らすことができるというわけだ。これはもちろんその分のメモリを開放するという意味もあるが、特にモバイル機器において不要な負荷を減らしバッテリーの寿命を伸ばすという効果があるのだと思う。

さて Event Pages では setTimeout/setInterval の代わりに chrome.alarms を使う。これにアラームを登録すると指定したタイミングにバックグラウンドがロードされ、次に chrome.runtime.onStartup に登録したイベントハンドラが呼ばれ、最後にアラームに登録したイベントハンドラが呼ばれる。

このアラームが特徴的なのは、登録が Chrome ブラウザに対して永続的であるということだ。定期的に発生するアラームは、エクステンションのインストール時にただ 1 度登録すればいいのである。PhoneticNews ではそれを知らず、アラームの登録をエクステンションのスタートアップ時に毎回行っていた。これが起動時に二重に読み上げが行われていた原因だ。

その他、従来は NHK ニュースだけに限っていたのを他のニュース配信社もいくつか含めるようにした。
phoneticnews-options
これ以外に、例えば共同通信や時事通信あたりも入れようかなと思ったのだが、なぜか日本の配信社は RSS フィードを公開していない、あるいはこっそりとしか公開していないところが多い。

アイコン押下時のポップアップでは最後に読み込んだ 1 件のニュースだけを表示していたのだが、最大 10 件にまで拡大した。
phoneticnews-popup

fixed: GRADIUS

gradius-on-edge

手持ちのプロジェクトをいろいろ整理しているのだが、ずいぶん前に作ったグラディウスのソースを読んでたら RequestAnimationFrame の使い方を勘違いしたまま数年経過してたようなのでとりあえず直してみた。若干スクロールがスムーズになった気のせいがするかもしれない以外は特に動作は変わっていない。というよりも、各種データをどこからどうやって生成したのかきれいさっぱり忘れているのでソースを近視眼的にいじるくらいしかもうできないのだが。

その他、Windows 10 の edge で動かないという話があったので


edge でも動作するよう確認。

edge 自身は…将来に期待という感じ。今夏の Windows 10 のアップデートで拡張機能の実装を含めて良くなるそうなので、そのときになったら wasavi のテストもしてみたい。

Hello, Windows 10

期限が近づいてきたので、Windows 7 マシンを 10 にアップグレードさせた。

アップグレードにはいくつかの方法があるが、最近の Windows 10 のビルドは新規インストール時に 7 のプロダクトキーを受容してくれるそうなので、そうした。つまり新しいハードディスクを取り付け、10 の ISO イメージを書き込んだ USB メモリからブートし、インストールし、7 のプロダクトキーを入力してデジタル権利付与なるものを得た。

今のところ未解決のトラブルは 1 つのみ: スリープさせると、その直後(大体 1 分以内)に勝手にスリープを解除しようとする。

その他は元の環境にあったソフトウェアを再インストールするのが面倒なくらいで、特に問題はない。というのも Windows 上の作業はだいたい VirtualBox の仮想マシンでやるので VirtualBox を入れて構成ファイルを元に戻せばそれで済んでしまう。強いて言えば、元の環境で地デジを見るのは DY-UD200 + TVTest という「ああ、アレね…」感あふれる構成だったのだが、それを Windows 10 上に再現するのがとても面倒だった。

Optimize a loading of assets

赤福プラスの話。

赤福プラス 3.x はふたばの画像掲示板を開いた時に DOMContentLoaded のタイミングで一旦ドキュメントをすべて書き換える。これはなかなかアグレッシブな動作で、3.x 未満の赤福プラスや、おそらくは他の、ブラウザ上で動作するふたば閲覧エクステンションはあくまでふたばがネイティブで送出するコンテンツに何かを「付け足す」ようになっていると思う。赤福プラス 3.x はそうではなく、完全に書き換える。元のドキュメントからメタな中間 XML ドキュメントを生成し、それを XSLT によって HTML に変換・再構築し、元のドキュメントに上書きする。

さて DOMContentLoaded というのは定義としては DOM の構造を構築し終わった段階で発火するイベントで、load イベントに比べてかなり早い段階で発生する。しかし、これはドキュメントに付随するスタイルシートやスクリプトや画像や iframe で指定されるサブフレームのドキュメントをまだ読んでいない段階というわけでは *ない*。もしかしたら並行して読んでいる途中かもしれないし、そうでないかもしれない。特に最新のブラウザほど、並行動作をしている割合が高い。

このブラウザの動作は、赤福プラスの仕様とは相容れない。もしその手のアセットを読み込むとしても、それは赤福プラスが管理する文書の下で読み込まれるべきで、その前段階でネットワークアクセスが発生するのは全て無駄になるため、好ましくない。

そういうわけで、その辺りを最適化するために以前から Presto Opera では外部の script 要素の動作をブロックしたりしていた。似たようなことは Chrome の WebRequest API でもできるわけで、やってみた。

やることは、赤福プラスがページを再構築している間、そのページから発生するふたば外へのリクエストをすべてブロックすることである。そのために、

  • バックエンドの main.js にタブ ID をキーとするハッシュ initializingTabIds を用意する
  • WebRequest API によりすべてのリクエストをリスンする
  • ふたばの画像掲示板の URL がアクセスされたら、そのタブ ID を initializingTabIds に記録する
  • その他の何かがアクセスされた場合、それが属するタブ ID が initializingTabIds に含まれていれば、リクエストをキャンセルする
  • 赤福プラスのフロントエンドがページを再構築を完了したら、バックエンドへ initialized メッセージを投げる
  • バックエンドが initialized メッセージを受け取ったら、そのタブ ID を intializingTabIds から削除する

という処理を加えた。

ところで WebRequest を使うには manifest.json の permissions に “webrequest” および対象となる URL パターンを含める必要がある。今回の場合は URL は http://*/* および https://*/*/ というかなり広範囲に及ぶ強いパーミッションを必要とする。その場合 webstore での審査もけっこう時間がかかるようだ。昨日の午後提出して今審査が通った。

ちなみに Firefox 版はそういう最適化をしていないので読み込みが一番遅い。気が向いたらがんばる。

Assign to a register

エディタを開いた時に特定のレジスタに常に任意の内容が入っているようにするには、vim では vimrc に

:let &z="foobar"

みたいにすると思う。この手のことは、wasavi ではまだできない(レジスタの内容自体は wasavi を閉じるときにエクステンションのバックエンドへ永続化されるので結果的に件のツイートの要件はすでに満たしている)。

できないというのは、vim の [cci]let[/cci] 文は、ex コマンドというよりは vimscript のいわゆる assignment statement で(ただしヘルプ上では expression command ということになっている)、wasavi にはまだスクリプティングの機能がないということだ。

もしその手の機能を設けるとしたら、ex コマンドに [cci]script[/cci] を新設して

:script wasavi.registers["z"] = "foobar";


:script << END wasavi.registers["z"] = "foobar"; END

な感じになると思う(スクリプトの文法がこうなるかどうかは未定だ)。vim のようにスクリプトの構造が ex コマンドのスーパーセットになっているような設計には、多分しない。

実は以前に script コマンドを実装しかけたことはあったのだが

  • wasavi の動作のうち、バックエンドにメッセージを送信して返事を待つ箇所は非同期だ。したがって、スクリプトも単にシーケンシャルに実行するわけにはいかない。つまり

    a = wasavi.registers["*"];
    print a;

    というようにはいかない。

    wasav.registers.get("*").then(function (content) {
    print a;
    });

    のような形式にせざるを得ない。すべてこの形式だとお気楽なスクリプトの範疇を超えている。同期か非同期かで文法を使い分けるとしてもそれも煩雑すぎる
  • スクリプトが例えば javascript であると定義するとそれを実行するのは eval 文になるが、それだとできることが多すぎる。何でもできすぎると動作の保証もセキュリティ面での安全性の保証もできない
  • 一方で完全に動作を制御可能なスクリプトインタープリタを実装し、その管理下でスクリプトを動かすとなるとコードサイズと実行速度の危険が危ない

などなど問題がありすぎてこりゃ時期尚早だということで棚上げになっている。どうしたものか。

Options page

前の記事で触れたように、かつては Chrome のエクステンションに関するオプションページは、1つのタブを消費するタイプだった。そのためのリファレンス

それが、最新のエクステンションの仕様では chrome://extensions のページにオーバーレイされる形で表示するようになっている。そのためのリファレンス。オプションページの構造自体は変わっていないのだが、表示される場所が違う。この場所の違いを吸収するため、[cci]chrome.runtime.openOptionsPage()[/cci] という API が新設されていて、manifest.json に記述されているオプションページの設定に従ってふさわしいそれを開くようになっている。

オーバーレイ表示といってもどうということはなく、要するに iframe みたいなものだ(実際には Shadow DOM や object 要素などを駆使したナウいものになっている)。ということは、やろうと思えば [cci]chrome.runtime.openOptionsPage()[/cci] を読んだ時、その時点のカレントタブにオーバーレイ表示することもできるはずだ。その方が無駄なタブ移動等々が少なくて混乱することはないのだが、実際はこの API を呼ぶと 必ず chrome://extensions を開いた上で(すでにそれを開いている場合はそれをアクティブにした上で)オーバーレイ表示する。これはまあきっと、偽装をさせないための制限なのだろう。

そういうわけで wasavi についても新しいオプションページで開くようにしてみたのだが、別にこれと言って新しくなったメリットはないわけで、リバートしてしまった。

wasavi and SSL

ペンディングになっていた件。つまり wasavi が参照する、appsweets.net 上のアドレスを http 化したいな、という件。そういったアドレスは 2 種類あって:

  • http://wasavi.appsweets.net/ – app モードのプレースホルダとなる URL
  • http://appsweets.net/authorized.html – オンラインストレージに接続した際のコールバック URL

これらを https 化するために、cloudflare の助けを借りることにした。cloudflare というのは CDN サービスだ。cloudflare 側にアカウントを作り諸々設定した上で appsweets.net ドメインに割り当てていたネームサーバを cloudflare が提供するそれに書き換える。そうすると appsweets.net を正引きすると今までは直接 xrea のサーバ s278 を指していたわけだが、cloudflare 内のサーバを指すようになる。このサーバがプロクシとして動作し、ブラウザ – cloudflare プロクシ – s278 という形でデータが流れる。このときメディアファイルについては cloudflare 側で適当にキャッシュされたりするのでその分 s278 側の転送量が抑えられる……等々のメリットがある。

そして SSL まわりだが、不思議なことに cloudflare に登録したドメインに SSL の証明書が即与えられる。従ってブラウザと cloudflare プロクシ間の接続については https でアクセスできるようになる。この時に使用するドメインはもちろん appsweets.net なので、結果的に appsweets.net が https でアクセスできるようになる。ちなみにここまで完全に無料である。なんか騙されたみたいな話だが、実際にこうなる。

そういうわけで、上記の 2 アドレスについても https を使えるようになる。

デメリットとしては appsweets.net の内容を cloudflare に 100% 預ける形で仲介させているので、悪意の有無を問わず、そちらで変な操作をされることが絶対にないとはいえない。SSL 化されるのはあくまでブラウザと cloudflare 間であって、cloudflare と appsweets.net 間は http でやりとりされる。従ってすごくセキュアというわけではない。ブラウザと cloudflare 間の SSL 接続はバーチャルホスト名ベースの割と新しい仕組み(SNI: Server Name Indication)を利用していて、古めの OS/ブラウザからはアクセスできない(たとえば、Presto Opera 12.17 ではアクセスできない)

Mysterious bound keyword

Firefox 46a だったか 47a だったか忘れたが、wasavi と赤福プラスが両方動かなくなったことがあって調べてみた所、

  1. 両方が使っている extension_wrapper.js 内で、それが Firefox + Add on SDK で動いているかどうかを判断している箇所があり
  2. Firefox のその該当バージョンで判断のもとになっている箇所が変更されたため

動かなくなっているということだった。

extension_wrapper.js は Chrome でも Opera でも Firefox でも共通して動くので、それぞれのブラウザのうちどれで動いているのかを判断しないといけない。Chrome なら window.chrome が存在しており、Opera なら window.opera が存在しているのでそれが判断の助けになる。。

一方 Add on SDK の PageMod における content script かどうか、というのを判断するためのあんしんあんぜんな方法はないように思える。とりあえずのところ、self.on が存在し、それが function であり、かつその toString() の値が “function on (.*?) { [native code] }” とかそんな感じになっていればまあたぶんきっと SDK ベースの content script だろう…というような判断をしている。”[native code]” がミソなのだが、これが最適解というわけでは全然ない。

これが、今回動かなくなってしまったのである。それは、self.on の toString() の値が “function bound on (” というように、謎のキーワード bound が入るようになっていたからだ。bound……って何?

Bringing a tab to the front

Chrome や Firefox は標準状態だと新規タブを開いたときそれをバックグラウンドにするのだが、個人的にはその仕様は使いにくく、フォアグラウンド(アクティブ)にしたい。Firefox の場合はそういうふうにするオプションが本体にあるので単にそれを使えばいい。しかし Chrome にはそういった標準的オプションはない。代わりにエクステンションを入れるしかない。いくらなんでもこの程度の機能は本体が持ってていいと思うんですけど。

というわけで、今までは Tabs to the front! というエクステンションを入れていた。

しかし、どうもこれが cVim と相性が悪い。Chrome 上で target=”_blank” なリンクをクリックして作成したタブは確かに常にアクティブになるのだが、cVim がエクステンションの chrome.tabs.* API を用いて生成したタブについては、アクティブになったりならなかったりとても不安定なのだ。

そこで、Tabs to the front! を使うのはやめ、cVim 側で対応することにした。cVim 側で新しいタブを開くのは単に t キーを押す。これは [cci]:TabNew[/cci] というキーストロークに仮想的にマップされているのだが、これを [cci]:TabNew![/cci] にマップし直すことで([cci]![/cci] を付ける)新しいタブがフォアグラウンドで開くことを指定する。これで解決!

と思ったら全然解決せず、不安定なままなのである。困ったことに cVim のバックグラウンドをデバッグし、実際に新規タブを生成するところで止めてみると確かに active パラメータは正しく渡されている。というかデバッガを起動している状態では思った通りの動作をするのだが閉じると不安定な状態に戻ってしまうのだ。ひっどい。つまりこれは、chrome.tabs.create() がなんかバグってんじゃないのかしらん?

この不具合は、しかし前述の通り Tabs to the front! を有効にしても直らない。このエクステンションは何をやっているのかといえば非常にシンプルで:

chrome.tabs.onCreated.addListener(function (tab) {
chrome.tabs.update(tab.id, {active: true});
});

とまあ実にこれだけなのである。要するに新しいタブが作成されたことを察知したら、すかさずそれをアクティブに更新している。

これが動いたり動かなかったりするというのは、おそらくは [cci]chrome.tabs.update()[/cci] と、chrome 内でタブを deactive にしている「何か」との、実行される順番がその時々で不定ということなんだろう。ということで、

chrome.tabs.onCreated.addListener(function (tab) {
setTimeout(function () {
chrome.tabs.update(tab.id, {active: true});
}, 100);
});

なんて感じに遅延を挟んでやると思った通りの動きになる。しかし Tabs to the front! は人様のエクステンションなのでいじることはできない。どうしたものかな。

タブの動作の変更といえば拙作の Tabqueue があったので、そっちに機能を持たせることにした。Tabqueue とは、あるタブを閉じた後にどのタブをアクティブにするかについて、Opera12 のようにタブをアクティブ化した順のキューに応じて決定するという Presto Opera への未練タラタラなエクステンションである。従来はその機能に絞った単機能のエクステンションだったので、複数の機能を持たせるにあたり新しくオプションページを作成し
tabqueue-options
それを通して有効・無効を指定するようにした。ちなみに設定は chrome.storage.sync に保存されるようにしたので、複数のデバイス間で同期する。

ところでいつのまにかエクステンションのオプションはこのようにオーバーレイ形式になっていた。従来の、タブを1枚消費するタイプは obsolete なのだという。知らなかったそんなの…。

そういうわけで更新した

wasavi and content type of a page

issue #128 と関連して。

discuz という PHP アプリケーションがある。これは要するに中国版 phpBB であって、phpBB 同様にいわゆるフォーラム機能を提供する。中国語圏内ではとても有名なのだそうだ。ざっと見た感じそれほど phpBB との機能差は見受けられなく、なんで似たようなものを一から作りなおす必要があるのかよく分からないが、まあそれは特に問題ではない。ちなみに disqus とは何の関連もない。

で、wasavi を有効にしていると discuz 上での post が上手くいかないというクレームが来たのである。それに対応してみたい。

まずダウンロードし、ローカルにインストールした。エンコーディングによって何種類かあるが、とりあえず繁体字・UTF-8版を持ってきて、適当なディレクトリに展開する。discuz はバックエンドとして MySQL を使用するので、あらかじめ適当なデータベース、それにアクセスするためのユーザアカウントとパスワードを作っておく。ブラウザから [cci]展開ディレクトリ/upload/install/[/cci] にアクセスし、適当に必要な項目(含データベース情報)を埋めつつウィザードを進めるとインストールが完了する。それにしても繁体字ならなんとなく雰囲気で読めるんじゃないかと期待したが、さっぱりわからないものです。登録、がログインのことなのだそうだ。そうなんだ…。

そんなわけでその環境で試してみると、たしかに上手くいかない。discuz はサーバへの post は ajax ベースであり、隠れた iframe を経由している。post の応答である XML ドキュメントを iframe に受けて、その結果が動作を左右する。ここで wasavi のエージェントが XML ドキュメントに対して余計な script 要素を追加してしまうと、Chrome がそれを XML とみなさなくなってしまい、XMLDocument プロパティが無効になってしまう(このへんは深くは追ってないけど)。その後 discuz 側が無効な XMLDocument プロパティを参照して例外で中断してしまう…という筋書きだ。つまりつきつめると XML 文書に対して wasavi のエージェントを動かしてしまうことがバグの原因だ。

これは正に issue #128 と関連している。前の記事では text/html だけではなく application/xml(あるいは、+xml を含むもの)に対しても実行したほうがいいのかなと思ったわけだが、今回のバグは実行してはダメなパターンなのである。

エージェントは wasavi を起動するために textarea などの HTML 要素を含む文書を対象としなければいけない。なので、単純に考えれば対象となる文書は text/html か、application/xhtml のいずれかだ。ただしもう一つ可能性があって、サーバからは xml が送られてくるが、ブラウザ上でそれを XSLT で html に変換するという構造のアプリケーションだ。この手のアプリケーションを、そういう構造だと判断する処理はかなりめんどくさい。

しかしそんなへんてこな構造のアプリケーション存在するんだろうかと考えると、うーんまあ、ないか。ないな。うんうん。ないない。

というわけでエージェントを実行すべき contentType は上記の通り html と xhtml だけにしよう。xhtml にしても生きてるのか死んでるのかよく分からない状態ではあるが…。