building a xpi

Firefox + Add-on SDK で 最終的に .xpi ファイルをビルドするには、

$ cfx xpi --pkgdir=/path/to/package --update-link=http://path/to/update-link --update-url=http://path/to/update-url

などとする。

cfx は、エクステンションを構成するファイルと、Add-on SDK が提供するライブラリをかき集めて .xpi ファイルを生成する。後者は、低位なライブラリ api-utils と高位な addon-kit の 2 種類あるのだが、addon-kit に関してはエクステンションが参照しているものだけを抽出して、.xpi のサイズの肥大化を防いでいる。なかなか賢いですね。

ただ、どうやってエクステンションが参照している addon-kit ライブラリを抜き出しているのかというと、$(addon-sdk)/python-lib/cuddlefish/manifest.py の中で、javascript ソースを正規表現レベルで走査している。

REQUIRE_RE = r"(?
という正規表現で、まさに grep している。javascript の構文解析をしているわけではない。

これがこうなっているのは、つまりこれでたいていの場合良いだろうということなんだと思うが、そうでもない場合もある。wasavi のパッケージは js ファイルを minify している。つまりパッケージ内の javascript ソースはものすごく長い 1 行のテキストなのだ。これが根本的な原因だと思うのだけど、参照しているライブラリの抜き出しがうまく行ったり行かなかったりする(たいていはうまく行かない)。

minify しなけりゃいいじゃんという気もするが、もしかしたら将来 closure compiler を使うかもしれないのでやっぱり cfx 側が変則的な javascript ソースにも対応してもらったほうがいいのではないかと思う。

とりあえずの回避策としてこうしている:

  • エクステンションで参照しているすべてのライブラリを内部的に require するダミーのモジュールを作る。lib/dummy-require.js とか。

    var self = require('self');
    var pageMod = require('page-mod');
    var tabs = require('tabs');
    var simplePrefs = require('simple-prefs');
    var clipboard = require('clipboard');
    var timers = require('timers');

    var locale = require('api-utils/l10n/locale');
    var xhr = require('api-utils/xhr');

    exports.hello = function () {
    return 'it is dummy module';
    }

    このソースだけは、minify しない。

  • 他の javascript ソースのどれか 1 つで、上記のソースを require する。ソースのできるだけ先頭の方がいいかもしれない。main.js あたりの先頭で:

    if (typeof window == 'undefined') {
    window = this;
    window.jetpack = {};
    require('./dummy-require');
    }

    のような感じ。require するだけで、別に何かに使ったりするわけではない。

    この状態で cfx xpi すると、必要なライブラリが正確に抜き出されるようだ。

    うーんまあ割とどうでもいい tips だな!

file i/o #10: final!

Opera をはじめ、Chrome と Firefox でも dropbox に対する読み書きを行えるようにした。

Chrome でも動くようにするのは、ぜんぜん難しくないのだ。んが、Firefox で動かすのがなかなか面倒。他のブラウザが、エクステンションといいつつも実態は html ページなのに対して、Firefox (Add-on SDK) で動くエクステンションのコードというのはグローバルオブジェクトが [object Proxy] だかなんだかという見慣れた window とはちょっと違う何かだったりする。

で、jsOAuth.js という独立したライブラリを参照する際、Opera と Chrome ではスタートアップとなる html ページにいつものよーに script 要素を書くだけでよいのだが、Firefox の場合は CommonJS に従ったルールで lib ディレクトリに置く必要がある。jsOAuth は Node.js & CommonJS コンパチブルだと謳っているので、まあ Firefox でもうまくいけばそのまま動くのだろうなどと思っていたら、甘かった。

CommonJS に則った方式で再利用可能なモジュールを書く場合、

exports.foo = function () {
console.log('hello, world');
};

などと exports に対して登録する形になる。jsOAuth もその先頭で

var exports = exports || this;
exports.OAuth = (function (global) {
:
})(this);

とやっている。これで、CommonJS 下ではエクスポート、一般的な javascript では window に OAuth オブジェクトが結び付けられる。めでたしめでたし……のはずが、なぜか Firefox ではいくら再利用する側で

var OAuth = require('./jsOAuth');

とやっても読み込まれない。正確には、読み込まれるのだが(つまり jsOAuth.js 自体は評価されるのだが)、再利用できない。OAuth は undefined のままなのだ。

結論から書くと

typeof window == 'object' && eval('var exports = this;');
exports.OAuth = (function (global) {
:
})(this);

のようにする必要がある。SDK が提供する javascript の実行環境は前述の object Proxy だか sandbox だかの、管理された空間みたいなのだが、その空間のグローバルに提供される exports やら require() やらはちょっと特殊な扱いっぽいのだな。なのでおそらく、var exports なんてしちゃうと本来の exports を「管理された空間上の exports 」で覆ってしまって役に立たなくなるということなのだと思う。したがって、exports の polyfill を定義するのは本当に window 下で実行されているときだけに厳密に分けないといけない。

そもそも require とか exports とか、どこでどう定義されているのかと SDK のソースをいろいろ探ってみたのだけど、どうも見つけられなかったのは内緒。いやまあ packages/api-utils/lib/loader.js とか cuddlefish.js とかがキーだと思うんだけど……。なんか bootstrap から追っていってもいきなり require() とか使い出してる感じだしよく分からなかった。

このほか、XMLHttpRequest も SDK 専用品を使わなければならなかったり(しかも upload プロパティまだ実装してまへんテヘペロ とかいう未完成品だったりする)、Firefox はなかなかいろいろと勝手が違うのだった。

file i/o #9: file, again

ex コマンド file でパス名を指定する。ところでおもしろいことに vim の場合、そのパス名がディレクトリそのものかどうかのチェックとかは行わない。
:file ../
なんてやると、素直にパス名が ../ になる。この状態で保存しようとしても、当然エラーになる。

これ、たとえば新しいパス名がディレクトリ部分しか含んでおらず、かつ現在のパス名がセットされている場合は変更前のファイル名を持ち越して使ってくれればいいのになーということでそうした。/foo/bar/baz.txt を開いて
:file ..
とするとパス名は /foo/baz.txt になる。この状態で
:file bar/
とすると /foo/bar/baz.txt になる。

こういったパス名のハンドリングは javascript でやるのはなんか新鮮だ。そしていろいろ考えることがある。

まず dropbox へ保存するとして、使えない文字があるのかどうか? windows の場合ファイル名に「:」を含めることはできない(もっとあったと思うけど忘れた)。「foo:bar.txt」なんかを dropbox へ保存できたとして、windows の dropbox クライアントでそれを同期するとどうなるのか? また保存できないとして、それを事前に知ることはできないのか? ちなみに cygwin でファイル操作するとなんと「:」を含めたファイル名を作れるのだが、どういうことか U+F03A という不思議な文字に置換される。私用領域。

相対パスの解決。さすがにこれを javascript で書いた例はぐぐっても見つからない。というよりむしろ、javascript じゃなくても相対パスを解決するアルゴリズムみたいなのがなかなか見つからない。これは考えてみれば当然で、相対パスの解決なんてシステムが提供するライブラリなりシステムコールで行うのが正道中の正道であって自前でやる方がおかしい。そんな中で URI に関して言えば、RFC1808 が URI 中の相対パスの解決アルゴリズムを記述している。とりあえずそれを参考にしつつそれっぽく解決するようにした。

file i/o #8: file

ex コマンド、file。
f[ile][file]
引数 file が指定された場合、現在のパス名を file へ変更する。

file コマンドはファイルの情報を表示する。ファイルが現在のパス名を持っている場合、それを含めて表示する。持っていない場合は、現在のパス名がないことを表示する。

編集バッファが行を持っている場合、現在の行番号と行の総数を含めて表示する。持っていない場合、編集バッファが空であることを表示する。

編集バッファが最後に書き込まれてから変更されている場合、そのことを含めて表示する。readonly オプションがセットされている場合、そのことを含めて表示する。

メッセージはその他のここに定義されていない項目を含めてもよい。

ということで、:f したり ^G したりすることで表示されるあれは割と実装によってまちまちだ。

  • vim 7.3 (香り屋版)
    "wasavi.js" [変更あり] 11622 行 --33%--
    "[無名]" --バッファに行がありません--
  • vim 7.3 (cygwin)
    "wasavi.js" 行 1 (全体 11622) --33%-- col 1
    "[無名]" --バッファに行がありません--
  • nvi 1.79
    wasavi.js: modified: line 2 of 11622 [33%]:
    /tmp//vi.ujMcQJZSaX: unmodified: empty file

表示する項目を大まかに分けると、

  • ファイル名。ファイルシステム上のパス名に割り当てられたそれか、割り当てられていないことを示す文字列
  • フラグ的なもの。変更済み、読み取り専用、等々。
  • バッファの状態。バッファ中のカーソル位置、またはバッファが空であることを示す文字列

ということになると思う。ということで、
"ファイル名" [フラグ的なもの] バッファの状態
ということにする。フラグが複数の場合はカンマで区切る。

こんな感じか。
とりあえずファイル操作系の ex コマンドはこれでだいたい実装したことになる。実はファイル名を入力する際の補完機能も作らないといけないのだが……。

file i/o #7: off-line

dropbox へ保存する際、15 秒程度の遅延を仕掛けている。タイマを設定し、保存対象となるテキストを適当なバッファへ覚えておき、15 秒後に保存処理を走らせる。その 15 秒以内に再度保存を要求された場合は単にバッファだけを更新する。

これは短時間のうちに何度も dropbox へアクセスするのを抑えるためだ。この、保存に際してバッファを経由する仕組みはオフラインアプリケーション化に繋がるかもしれない。バッファをローカルの何らかの永続的ストレージに持たせる。バッファから dropbox への送信はオンライン時にのみ行う。オフ -> オンラインに遷移した場合は自動的に溜まったバッファを送信する。そんな感じにすると常時接続ではないネットワーク環境でもそれなりにストレスレスな編集ができるかもしれない。

まあすぐそういう風にするわけじゃないけど。一応覚えておこう。

file i/o #6: write

結局のところ、HTTP PUT で保存するようにした。

jsOAuth はリクエストを生成するためのメソッドとして、より高位な順に

  • fetchRequestToken()、fetchAccessToken()
  • get()、post()、postJSON()
  • request()

を提供している。PUT はないので新しく put() を書くか、request() を直接呼び出す必要がある。が request 自体が PUT に微妙に対応し忘れている箇所があるのでそこはちょこちょこっと修正する必要はある。

さて jsOAuth を使ったとしても、dropbox にアクセスするトランスポートはつまるところ XMLHttpRequest なのだが、xhr で PUT する場合、つまり open() 時に ‘PUT’ を指定して send() 時に文字列を引数にした場合、その文字列は UTF-8 のバイト列に変換され、リクエスト自体が Content-Type: text/plain;charset=UTF-8 とみなされる(仕様)。これでは、UTF-8 以外のエンコーディングに対応できない。

send() の引数として文字列ではなく、任意のエンコーディングのバイト列に変換した ArrayBuffer にすれば、この制限は迂回できる。最新の API の実装度に関しては Chrome と Firefox に対してだいたい 2 周遅れであるところの Opera もかろうじて ArrayBuffer くらいは対応している。しかし javascript では自前のテーブルを用意するほかに UTF-16 から任意のエンコーディングへ変更する手段がない。どのブラウザも内部では UTF-16 でテキストを保持し、自身がさまざまなエンコーディングを UTF-16 へ変換するための機構を備えているのだから、それを利用したインターフェースを javascript に公開してくれてもいいのに、ないのだ(Firefox はたぶんある)。

これを解決するには、dropbox とのやり取り自体を、ブラウザと dropbox 間で直接ではなく、ここのサーバを経由させ、サーバ上で適宜適当なエンコーディングへ変換させつつ行う必要がある。ブラウザと dropbox とのやりとりを代理で行うというのはつまり、OAuth を用いたアクセスを代理で行うということだ。サーバ上で OAuth アクセスを行うには単にこれを利用させていただくだけでいい。そして実はこの方が、dropbox から割り当てられたコンシューマ キーとコンシューマ シークレットをエクステンションのソース内に埋め込まなくてもいい(そのあたりの質問)のでむしろ筋がよかったりする。

でもなー dropbox とのやり取り自体速いわけでもないのに、ここのサーバを経由させるもっともっさり感漂うことになってしまうなー……。

file i/o #5

edit コマンドも作った。+command もある。でもこれ、ワンライナーで vi コマンド実行するときくらいしか使わないよね。vim 独自の ++command はエンコーディングを変えて読み込みなおす際に結構使うけど。エンコーディングに関してはまだちょっと分からない点があるので保留。

なおファイル名だけど、[scheme]/[path] という形にする。dropbox 上のファイルの場合は dropbox://foobar.txt みたいな感じになる。複数のオンラインストレージに対応した場合に、スキームを省略した場合はどうするか? たぶん wasavi のオプションページで「スキームを省略した場合の規定のストレージ」みたいな感じで適当なものを設定することになると思う。また、ex コマンドで

:changescheme dropbox

のように動的に切り替えられるようにする、かもしれない。

ということで次は保存だ。REST api で dropbox へ保存するには URL /files/path へ POST するか、/files_put/path[?options] へ PUT するかの 2 通りの方法がある。汎用的なのが前者で、簡略化されているのが後者という扱いだ。いずれにしても、保存するのだからバッファの内容を送信するわけなのだけど、前者は Content-Type:multipart/form-data で送る必要があり、また後者はもちろん PUT リクエストを行える必要がある。

が、jsOAuth の場合、ファイルの送信は POST する場合は FormData に依存する、つまり <input type=”file”> を経由してユーザがローカルファイルシステム上のファイルを指定する必要がある。従って javascript のみで完結させることができない? PUT については、そもそも jsOAuth が PUT で送る手段自体を提供していない。結局のところこの辺は jsOAuth 自体をいろいろ弄る必要があるようだ。

file i/o #4

ex コマンド read コマンドは動くようになった。
内部の動作ではなく、操作の流れを主体にすると以下のようになる:

  1. :r file nameと入力
  2. 別タブで dropbox 上のページが開き、api へのアクセスの許可を求められる
  3. 許可する。ページは自動的に閉じ、wasavi を開いているタブが再度アクティブになる
  4. ファイルが読み込まれるまで待つ。認可のフェーズが進むごと、あるいは読み込みが適当に進むごとにステータスライン上の進捗メッセージが更新される
  5. 読み込まれる

もちろん、一度認可を済ませていればあとはまったくローカルで vi を動かしているかのように read コマンドは振る舞う。まあさすがに dropbox からの読み込みは一瞬とは行かないが……。

さて read コマンドは、ファイルアクセス系のコマンドの中でもかなり単純な部類で、本質的にはファイルシステムからのペーストみたいなものだ。ただ以下のことを考える必要はある:

  • 改行コード。今更言うまでもないが、\r と \n と \r\n 系がある。\r は OS X より前の Mac OS だろうから対応する意味はないかもしれない。行ごとの読み込み時の改行コードを覚えておいて書き出し時にはそれを再利用するのと、新規に行を挿入する場合は読み込み時に最も多かった改行コードを用いる、あたりがポイントか? もっと単純に単体のファイルには 1 種類の改行コードのみが使われると決め打ちしてもいいかな
  • エンコーディング。世の趨勢、特に web 系は UTF-8 固定でいいじゃんという感じになりつつあるようなないような感じだが、まだ他のエンコーディングを一切考えなくてもよいというわけでもない。でも javascript でエンコード変換って重いしテーブルはでかいしどうもなー。appsweets.net 上に適当なプロクシサービスを置いてそれを利用するか? しかしそういったプロクシを通すなら、そもそも OAuth の処理自体もサーバでやったほうがいいじゃんという気も……

それはそれとして、読み込みの次の段階は edit コマンドだ。こちらは read に比べるとなかなか手ごわい。

e[dit][!][+command][file]
コマンド名に「!」が追加されておらず、かつバッファが変更済みの場合、エラーになる。

file が指定されている場合、バッファの内容は file の内容で置き換えられ、file のパス名がバッファに関連付けられる。file が指定されていない場合、バッファの内容はバッファに関連付けられたパス名の最新の内容で置き換えられる。何らかの理由で file の内容にアクセスできない場合、バッファは空になる。

+command オプションは空白で区切られる。+command 内の空白文字はバックスラッシュを前置することでエスケープできる。+command はバッファの内容が置き換えられ、現在行と現在桁がセットされた直後に ex コマンドとして実行される。

現在行と現在桁は以下のようになる:

  • バッファの内容が空である場合:
    現在行は 0 になる
    現在桁は 1 になる
  • そうではなく、ex コマンドモードで実行されたか、もしくは +command 引数が指定されている場合:
    現在行はバッファの最終行になる
    現在桁は最初の非空白文字になる
  • そうではなく、file が省略され、現在のパス名が用いられた場合:
    現在行はバッファの先頭行になる
    現在桁は最初の非空白文字になる
  • そうではなく、file が以前に編集されたファイル名の場合:
    現在行は最後に編集した位置を覚えていればその位置になる。覚えていないかその値がバッファの新しい内容において不正である場合は、バッファの先頭行になる
    現在桁は最後に編集した位置を覚えていればその位置になる。覚えていないかその値がバッファの新しい内容において不正である場合は、最初の非空白文字になる
  • そうではない場合:
    現在行はバッファの先頭行になる
    現在桁は最初の非空白文字になる

a new version “marlin” is approaching fast

Opera 12.50 だそうです。

Clipboard APIDOM3、その他を実装するとか。

おお wasavi を動かす上で他のブラウザから遅れている機能が一気に揃うとは……! Composition events も実装するとは確約されていないけど、実装されれば Opera でも wasavi がちゃんと動くようになるなあ。

その他、Opera Labs では SPDY 対応ビルドが公開されている。へー。

 * * *

インストールして試してみた。Composition event が発生しないじゃないですかー! やだー!

file i/o #3: hey, poll

OAuth を用いた api の使用認可を行うわけなので、途中で dropbox.com のページ上で wasavi アプリケーションに対してアクセスを許可していいか? と聞かれる。ページには「許可」ボタンと「拒否」ボタンがある。また、拒否の意味を込めてページを閉じる場合もありえる。あるいは件の認可に関係のない別のページへ遷移する場合もある。

  1. 許可ボタンをアクセスした場合はその情報がページから dropbox へ送信される。サーバからのレスポンスで oauth_callback で指定した url へリダイレクトされる
  2. 拒否ボタンをアクセスした場合はその情報がページから dropbox へ送信される。ページは dropbox.com/home へリダイレクトされる
  3. ページを閉じた場合は、当然だがそのタブは消滅する
  4. 他のページへ遷移した場合はそのタブが指すアドレスが変化する

そういったユーザのアクションをエクステンションのバックグラウンド側で受け取りたい。wasavi の実行はユーザのアクションを待って止まっている状態なので、許可であれ拒否であれ全てのアクションを認識する必要がある。

コールバックで指定した url に対してコンテントスクリプトを仕掛けておく単純な方法では、1. 以外を受け取れない。どうするかなー。

 * * *

ということで表題の通り、ポーリングすることにする。とりあえず Opera に限って考えると、タブに新しい文書が読み込まれるごとにバックグラウンド側の opera.extension.onconnect でイベントを取れる。この中で、https://www.dropbox.com/1/oauth/authorize を指す文書が生成されたら opera.extension.tabs.getAll() でタブの管理情報(BrowserTab オブジェクト)みたいなものを得る:

opera.extension.onconnect = function (e) {
var targets = [];
opera.extension.tabs.getAll().some(function (tab) {
if (tab.port == e.source) {
targets.push({tab:tab, startUrl:tab.url, callback:function () { ... }});
return true;
}
});
};

この管理情報は、スナップショットではなくライブなので、これを監視すればナビゲーションの変化を追跡できる。適当な間隔(1秒くらい)でタイマを動かし、タブが指す url が変わっていたら認可処理を続行する。特に、コールバック先の url になっていたら api の使用を許可されたとみなすことができる(もちろんリクエストトークンを受け取ってユーザの認可待ちという oauth の内部状態とも合致する必要がある)。