Now browsing the SCRAPBLOG weblog archives.

ファイル書き込み処理を別スレッドで行う

modest に投稿した記事と同内容です。

注意: この記事の内容は Firefox 3.6 以降で追加される新機能について触れています。

Firefox ではブラウズ中のセッション状態を保存するために、デフォルトで10秒に1回、JSON形式のデータをプロファイルフォルダ下の sessionstore.js へ書き出す処理を行っています。
しかし、 Firefox 3.5 まではこの処理が原因で YouTube の動画閲覧中にプチフリーズが頻発するといった現象が見られたようです。そこで、 Firefox 3.6 以降では、ファイル書き込み処理を別スレッドで行うことで、このプチフリーズが発生しないよう改善されることになりました (Bug 485976 – Move writing sessionstore.js off the main thread)。

この別スレッドでのファイル書き込み処理は nsIAsyncStreamCopier という XPCOM にて実装されていますが、 NetUtil.jsm という JavaScript モジュールをインポートすることで、拡張機能などから簡単に利用することができます。

サンプル

以下、別スレッドにてファイルへ文字列を書き出すサンプルを作ってみます。なお、ソースコード中の Cc, Ci は、それぞれ Components.classes, Components.interfaces への参照です。

最初に、 JavaScript モジュールをインポートします。当然インポートは最初に一度だけ行えば良く、ファイルへの書き出しを行うたびに行う必要はありません。

Components.utils.import("resource://gre/modules/NetUtil.jsm");

次に、書き出し先のファイル(nsILocalFile オブジェクト)を生成します。なお、変数 path の値は各自の環境に合わせて適宜修正してください。

var path = "C:***.txt";
var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(path);

次に、 nsISafeOutputStream によって安全にファイルへ出力するためのストリームを生成します。どういうことかと言うと、ファイル書き込み中は「test-1.txt」のような別名の一時ファイルへ書き込み、書き込みが完了したら本来の「test.txt」へ上書きすることで、ファイルが破損しにくい仕組みとなっています(参考)。

var ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
              createInstance(Ci.nsIFileOutputStream);
ostream.init(file, -1, -1, 0);

次に、ファイルへ書き込む文字列から、入力用のストリームを生成します。

const TEST_DATA = "this is a test string";
var istream = Cc["@mozilla.org/io/string-input-stream;1"].
              createInstance(Ci.nsIStringInputStream);
istream.setData(TEST_DATA, TEST_DATA.length);

なお、日本語を含む文字列を UTF-8 エンコードでファイルへ書き出す場合、以下のように nsIScriptableUnicodeConverter を使って入力用ストリームを生成します。

const TEST_DATA = "これはテスト用文字列です";
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var istream = converter.convertToInputStream(TEST_DATA);

最後に、 NetUtil.asyncCopy を使い、入力用ストリームを出力用ストリームへコピーし、別スレッド上でファイルへの書き込みを行います。3番目の引数は別スレッドでのファイル書き込みが完了した際に呼び出されるコールバック関数です。

NetUtil.asyncCopy(istream, ostream, function(result) {
    if (Components.isSuccessCode(result))
        alert("ファイル書き込み成功");
});

今回のサンプルでは書き出す文字列が少ないため、別スレッドで処理が行われていることを体感できないと思います。そこで、以下のように長大な文字列を生成して試してみると、 Firefox がフリーズすることなくファイルへの書き出しが行われることが体感できるかと思います。ただし、 for ループ自体が重いため、ファイル書き出し前にフリーズが発生します。

var TEST_DATA = "";
// ループ回数を少しずつ増やしながら調整してください
for (var i = 0; i < 100; i++) {
    TEST_DATA += "this is a test string
";
}

リファレンス

TOP

ダウンロードマネージャに進捗状況を表示させつつダウンロードする

modest に投稿した記事と同内容です。

拡張機能にて、ある URL からファイルをダウンロードするには、 Downloading Files – MDC で解説されているように nsIWebBrowserPersist::saveURI を使うのが一般的です。この方法でダウンロードをすると、ダウンロードマネージャの UI 上に進捗状況が表示されず、バックグラウンド処理のような感じでダウンロードが進行します。

では、ダウンロードマネージャに進捗状況を表示させつつダウンロードするには、どうすれば良いのでしょうか?そのためには、 Firefox 3 以降で導入された nsIDownloadManager インタフェースの API を利用します。

ダウンロードマネージャ

ここからは例として Google のロゴ画像をダウンロードし、ローカルファイルとして保存する手順を解説します。なお、ソースコード中の Cc, Ci は、それぞれ Components.classes, Components.interfaces への参照です。

まず、ダウンロード元のURLから、 nsIURI オブジェクトを生成します。

var sourceURL = "http://www.google.com/intl/en_ALL/images/logo.gif";
var ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var sourceURI = ioSvc.newURI(sourceURL, null, null);

次に、保存先ファイルのパスから、 URL が file:// 形式の nsIURI オブジェクトを生成します。
なお、変数 targetPath にセットするファイルパスは、各自の環境に合わせて適宜修正してください。

var targetPath = "C:***.gif";
var targetFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
targetFile.initWithPath(targetPath);
var targetURI = ioSvc.newFileURI(targetFile);

次に、ダウンロードを行うための nsIWebBrowserPersist のインスタンスを生成します。 persistFlags プロパティには、お好みに応じてフラグを設定してください。今回は、保存先ファイルがすでに存在する場合は上書きするフラグ、キャッシュを使わずに最新のデータをダウンロードするフラグ、 gzip 圧縮などがされている場合に自動で展開するフラグの3つを設定します。

var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].
              createInstance(Ci.nsIWebBrowserPersist);
persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
                       Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE |
                       Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;

いよいよ、今回の要となる nsIDownloadManager のサービスを呼び出し、 addDownload メソッドによってダウンロードマネージャへ新しいエントリを追加します。 addDownload メソッドの個々の引数についての説明は、下記コード中のコメントを参照ください。

var dlMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
var dl = dlMgr.addDownload(
    Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,    // ダウンロードマネージャ上での表示形式
    sourceURI,    // ダウンロード元の nsIURI オブジェクト
    targetURI,    // 保存先ファイルの nsIURI オブジェクト
    null,    // ダウンロードマネージャ上での表示名。 null なら保存先ファイル名となる。
    null,    // nsIMIMEInfo オブジェクト。詳細不明だが null で問題なし。
    Math.round(Date.now() * 1000),    // ダウンロード開始時刻。現在時刻を指定すればよい。
    null,    // 一時ファイルを作ってダウンロードする際に nsILocalFile を指定する。
    persist    // 先ほど生成した nsIWebBrowserPersist オブジェクトを渡す。
);

addDownload メソッドの戻り値は、ダウンロードマネージャにより管理される個々のエントリに対応した nsIDownload オブジェクトとなっています。この nsIDownload オブジェクトから nsIWebBrowserProgressListener インタフェースを呼び出して以下のようにすると、 nsIWebBrowserPersist 側のダウンロード進捗状況の変化がダウンロードマネージャ側へ伝わるようになります。

persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);

最後に、 nsIWebBrowserPersist::saveURI メソッドを実行し、実際にダウンロードの処理を開始させます。なお、 saveURI の引数にダウンロード元の nsIURI オブジェクトと保存先ファイルの nsILocalFile オブジェクトを渡す必要がありますが、それぞれ nsIDownload オブジェクトの source, targetFile プロパティから参照可能です。もちろん、これまでの一連の処理で登場した変数 sourceURI, targetFileの2つを渡しても構いません。

persist.saveURI(dl.source, null, null, null, null, dl.targetFile);

以上のような手順でダウンロードマネージャと連携しつつダウンロードした場合、単に nsIWebBrowserPersist::saveURI を使ってダウンロードした場合とは異なり、ダウンロード中に Firefox を終了させても再起動時に自動的にレジュームが開始されるというメリットがあります。したがって、巨大なファイルをダウンロードするような拡張機能では利用価値の高い方法となるかもしれません。

saveURL ヘルパー関数

ここまでかなり長いコードを書いてダウンロードマネージャに進捗状況を表示させつつダウンロードする方法を解説しましたが、実はブラウザウィンドウ (browser.xul) のように chrome://global/content/contentAreaUtils.js が読み込まれているウィンドウ内であれば、 saveURL というヘルパー関数を使って以下のようにいとも簡単に実現可能です。

saveURL("http://www.google.com/intl/en_ALL/images/logo.gif", "logo.gif", null, true, true, null);

5番目の引数を false に変えることでファイル選択ダイアログを表示させることなどもできますし、実際の拡張機能ではこの saveURL 関数を使うケースの方が多いかもしれませんね。

関連ドキュメント

TOP

Extend Firefox 3.5 Runners-Up 賞品

先日 Extend Firefox 3.5 の賞品が届いた。

またメッセンジャーバッグかよ、と思っていたが、中身はEeePC 1005HA (Seashell)。
なんでも、当初予定されていたメッセンジャーバッグが在庫切れだったらしい。

箱を開けると、真っ黒のEeePCが。

しかし、キーボードは英語配列!

まだ電源は入れていないが、当然OSも英語版Windows XPなんだろうな。
Ubuntuでもインストールして遊ぶか。

TOP

userChrome.js スクリプト集のページ

Firefox 2 の時代に作った userChrome.js スクリプト集のページ に掲載したスクリプトの一部が最近の Firefox 3.0 や 3.5 で動作しなかったため、修正しました。

変更点:

  • 「Paste And Go」が Firefox 3.5 以降で動作しないバグを修正。
  • 「Open Add-on Folder」でプラグインなどの項目を右クリックしたときに「Open Folder」メニュー項目を表示しないように改善。
  • 「Restart Firefox」のショートカットキーの変更と再起動処理の改善。
  • 「Colorful Tabs」が Firefox 3.0 以降で動作しないバグを修正。
  • すべてのスクリプト中に @compatibility で Firefox の対応バージョンを明記。
  • すべてのスクリーンショットを Firefox 3.5 のものに差し替え。

TOP

Webページの拡大率を取得する

拡張機能などから、ブラウザのコンテンツエリアに表示された Web ページの拡大率(画像も含めたズームイン・ズームアウトによる拡大率)を調べる。
以下、 browser.xul へオーバーレイした状態を想定。

Firefox 3.5 以前

nsIMarkupDocumentViewer::fullZoom で取得可能。

var zoomFactor = gBrowser.mCurrentBrowser.markupDocumentViewer.fullZoom;

Firefox 3.6 以降

nsIDOMWindowUtils::screenPixelsPerCSSPixel でも取得可能。

var win = window.content;
var winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
var zoomFactor = winUtils.screenPixelsPerCSSPixel;

両者の値の違い

Firefox 3.6 以降の screenPixelsPerCSSPixel で取得できる値は、厳密にはズームイン・ズームアウト機能の拡大率ではなく、 CSS での1ピクセルがスクリーン上で何ピクセルとして表示されるかを示した値である。そして、両者の値はなぜか微妙に異なる。 Firefox 3.5 で Firefox 3.6 とほぼ同等の値を取得するにはどうすれば良いか? All-in-One Gestures が面白いやり方で実践していた。

var doc = window.content.document;
var div = doc.createElementNS("http://www.w3.org/1999/xhtml", "DIV");
div.style.position = "absolute";
div.style.top = "100000px";
doc.body.appendChild(div);
var zoomFactor = (doc.getBoxObjectFor(div).screenY - doc.getBoxObjectFor(doc.body).screenY) / div.offsetTop;
doc.body.removeChild(div);

ダミーの DIV 要素を、 Web ページの document.body を基点とした上から100000px(CSS上のピクセル)の絶対位置に配置し、 body と DIV 要素のスクリーン上のY座標の値を getBoxObjectFor で取得し、両者の差を100000で割ることで算出している。 getBoxObjectFor を使用しているので、もちろん Firefox 3.5 以下限定である。

関連記事

SCRAPBLOG : Firefox 3 のフルページズーム使用時はスクリーン上でのピクセル量とCSS上でのピクセル量は一致しない

TOP

Firefox 3.6 にて HTMLElement.classList が実装

Firefox 3.5 以前

HTML中のある要素のクラス名を取得するには、 HTMLElement.className プロパティを使用する。
クラス名はスペース区切りで複数の値を指定可能であるので、クラス名にある値が含まれるかどうかを判定するには、

var elt = document.getElementById("test");
elt.className.indexOf("foo") >= 0;

のようにしてやればよいが、これでは foobar のような値が含まれている場合も true と判定されてしまう。

Firefox 3.6 以降

Firefox 3.6 にて導入された HTML 5 の仕様の一部である HTMLElement.classList プロパティにより、複数の値が指定されたクラス名の扱いが簡単になる。
HTMLElement.classList プロパティは、 DOMTokenList 型オブジェクトであり、 contains メソッドによってリスト中にある値が含まれるかどうかを正確に調べることができる。

var elt = document.getElementById("test");
elt.classList.contains("foo");

add メソッドや remove メソッドによってクラスに値を追加・削除したり、 toggle メソッドで値の有無を切り替えたりすることも可能。
クラスの個々の値を取得するには以下のようにする。

for (var i = 0; i < elt.classList.length; i++) {
    elt.classList.item(i);
}

TOP

Babelzilla の WTS で全ローカライズの進捗が100%と誤認識される問題

事象

Bazelzilla の Web Translation System (WTS) では、通常各ローカライズの DTD ファイルおよび propertiesファイル中の未翻訳部分は、その行自体が無い状態でXPIにパッケージングしてアップロードする必要がある。しかし、誤って全ローカライズの未翻訳部分を英語に置き換えた内容の XPI ファイルをアップロードしたところ、全ローカライズの翻訳進捗率が100%と誤認識されてしまった。もう一度正しい XPI ファイルをアップロードしなおしてもサーバー側のデータベースに以前翻訳された内容が保持されているようで、翻訳進捗率は100%のまま。こうなってしまうと、翻訳者はどのファイルのどの部分が未翻訳であるかを把握できなくなってしまう。

復旧方法

XPI ファイルからすべてのローカライズ( jar ファイル中の locale フォルダと chrome.manifest の locale 宣言)を取り除いた偽バージョンを作成し、アップロードする。その後、改めて正しい XPI ファイルをアップロードする。この手順で翻訳進捗率の表示が正しい値に戻った。

TOP

Firefox を再起動する

Firefox 3 以前

Firefox を再起動するとき、 Firefox 3 までは以下のようなコードを書く必要があった。

var appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
appStartup.quit(appStartup.eRestart | appStartup.eAttemptQuit);

ただし、上記コードでは Firefox 終了直前に「開いているタブを保存して次回起動時に復元しますか?」の確認ダイアログを表示する設定になっていた場合などに、ダイアログを表示せずに強制的に終了してしまう欠点がある。そのような場合でもちゃんとダイアログを表示させるようにするためには、 chrome://mozapps/content/extensions/extensions.js の restartApp 関数のように少々長いコードを書く必要があった。

Firefox 3.5 以降

FUELextIApplication インタフェースに追加された restart メソッドで実現可能となった。

Application.restart();

Application 定数が未定義の JS XPCOM 内では、以下のようにする。

var fuelApp = Cc["@mozilla.org/fuel/application;1"].getService(Ci.fuelIApplication);
fuelApp.restart();

また、 restart メソッドの戻り値で、実際に再起動の処理を開始するか、あるいはユーザによってキャンセルされたかを判別可能。

var ret = Application.restart();
alert(ret ? "再起動します。" : "再起動はユーザによってキャンセルされました。");

なお、 Firefox を終了する処理についても、 FUEL で実現可能となった。

Application.quit();

TOP

Windows Vista のバックアップと復元センター

Windows Vista のバックアップと復元センターをあらためて試し、バックアップ前と復元後の全ファイルを比較したところ、下記の拡張子を持ったファイルなどがバックアップ対象外となっていたようだ。

asp bak bat cgi chm dll exe ini jar js jsp manifest php pl vbs wsf

ちゃんとしたバックアップツールが無いかと探したところ BunBackup が良さそうなので乗り換えた。

TOP

拡張機能開発の一時休止のお知らせ

PCリカバリに伴い Windows Vista のバックアップと復元機能を使用したところ、「js」を含む特定の拡張子のファイルがごっそり消えたため、拡張機能開発をしばらくの間休止します。

TOP