前の記事では、Windows 10 Fall Creators Update以降のWSL(Windows Subsystem for Linux)を日常的に使いやすいようにするための環境整備のポイントをいくつかご紹介しました。これは、「何か作業をするときに、Windows上でLinuxのコマンド体系が使える小窓を開いて、その中でLinuxらしい(Windowsらしくない)やり方で作業する」という使用形態を想定した解説でした。
一方、Windowsアプリを主体として使用する場面でもWSLは活用できます。例えばEmEditorというテキストエディタは任意のアプリケーションを登録して呼び出せますが、ここにWSL経由でLinuxのコマンド列を実行するように設定すれば、EmEditorをLinuxのコマンドやシェルスクリプトで機能強化することができます。
なお、この記事ではWSLの使用手順そのものについては紹介していません。まだ準備が整っていない場合は、前の記事などを参考にまずWSLを有効化し、Ubuntuなどのディストリビューションをインストールしておいて下さい。
WSLを有効化済みの環境では、Windowsのコマンドプロンプトでwslconfig
とwsl
という2つのコマンドが使えるようになっています。
wslconfig /list
というコマンド列を実行すると、その環境に既にインストール済みのWSL用Linuxディストリビューションの一覧が表示されます。以下は、Ubuntuだけがインストールされた環境での実行結果の例です。
C:\Users\user> wslconfig /list
Ubuntu
インストール済みのディストリビューションの一覧をこの方法で確認した後、wslconfig /s (表示されたディストリビューション名の中の1つ)
というコマンド列を実行すると、そのディストリビューションがWSLでの既定のディストリビューションになります。
C:\Users\user> wslconfig /s ubuntu
このように既定のディストリビューションが登録済みの場面では、wsl
コマンドを実行するとWindows側でのカレントディレクトリに相当するディレクトリをカレントディレクトリとした状態で、WSL上のLinuxディストリビューションのシェル(Bash)を起動できます。
C:\Users\user> wsl ←Windowsのコマンドプロンプトでwslコマンドを実行
user@ubuntu:/mnt/c/Users/user$ ←カレントディレクトリでWSL上のシェルが起動する
user@ubuntu:/mnt/c/Users/user$ exit
C:\Users\user> ←シェルを終了するとコマンドプロンプトに制御が戻る
また、wsl
コマンドに引数を渡すと、その内容がそのままWSL環境のシェル上で実行されて、すぐにシェルが終了します。つまり、Windowsから直接Linuxのシェルのコマンドを実行できます。
C:\Users\user> wsl ls
スタート メニュー NTUSER.DAT ←WSL上のシェルでのlsの実行結果が出力される
...
C:\Users\user> ←その後、すぐにWindowsのコマンドプロンプトに制御が戻る
ただし、この方法で実行できるのはコマンド1つだけです。パイプラインを使った複雑なコマンド列を実行したい場合、あらかじめ組み立てておいたコマンド列をダブルクォートで括って文字列にし、bash
コマンドの-c
オプションで実行するという工夫をする必要があります。
C:\Users\user> wsl bash -c "echo 'hello' | sed -r -e s/h/H/"
Hello
C:\Users\user>
前述のwsl
コマンドの実体はC:\Windows\System32\wsl.exe
です。外部アプリケーションを呼び出す機能を持ったアプリケーションからこのファイルを実行するようにすれば、WindowsアプリケーションからWSL経由でLinux環境のコマンドを実行する事ができます。
例えばEmEditorというテキストエディタは、無料版での使用においても、「ツール」→「外部ツール」→「外部ツールの設定」で任意のアプリケーションを登録できます。以下は、これを使って選択範囲の文字列をBashのワンライナーで加工する例です。
選択範囲の文字数を数える
C:\Windows\System32\wsl.exe
bash -c "nkf -w | wc -c"
選択テキスト
ツールチップとして表示
システム既定(932, shift_jis)
選択範囲の行数を数える
C:\Windows\System32\wsl.exe
bash -c "nkf -w | wc -l"
選択テキスト
ツールチップとして表示
システム既定(932, shift_jis)
選択範囲の行を並べ替える
C:\Windows\System32\wsl.exe
bash -c "nkf -w | sort -n | nkf -s"
選択テキスト
選択範囲と置換
システム既定(932, shift_jis)
「実行ファイルとしてwsl.exe
を指定してbash -c "〜"
でコマンド列を渡す」という点と、「標準入出力をnkf
などを使って変換して呼び出し元のWindowsアプリに合わせる」という点が重要です。これらの点に気をつければ、gem
やnpm
でインストールされたコマンドなども容易に組み合わせられるため、応用の幅は無限大と言えます。また、ワンライナーで記述するのが大変な処理は、あらかじめWSLの環境にシェルスクリプトを用意しておきそれをC:\Windows\System32\wsl.exe bash "/home/user/script.sh"
のように実行するようにしてもよいでしょう。
以上、WSLをWindowsアプリケーションの機能強化に使う手順を、EmEditorでの設定を例としてご紹介しました。
gem
やnpm
でインストールできるコマンドの中には、特に明記はされていなくても、LinuxやmacOSの環境のみを想定して作られているという物が度々あります。特に拡張モジュール(バイナリ)を含む物はその傾向が顕著で、Windows上で動作させるにはVisualStudioの導入が必要であるなど、気軽に使えるとは言い難いのが実情です。
しかしWSL上のLinux環境であれば、そのようなコマンドも難なくインストールできます。「Windows上でgem
やnpm
を使う」場面に特有の苦労をしなくてもよくなりますので、使用にあたってのハードルが大きく下がりますので、今まで「便利そうなコマンドだけど、Windowsじゃ動かないんだよな……」と諦めていた人は、是非一度試してみて下さい。
PGConf.ASIA 2017でRUMの存在を知った須藤です。RUMはGINと違って完全転置索引にできるので全文検索用途によさそう。(Groongaは元から完全転置索引にできるのでずっと前からよかった。)
関連リンク:
PostgreSQL Conference Japan 2017での内容にPGroonga 1.0.0からPGroonga 2へのアップグレード関連の話を盛り込んだ内容になっています。なお、PostgreSQL Conference Japan 2017での内容は次の昨日の実現方法を紹介でした。
これらの機能の実現方法はPostgreSQL Conference Japan 2017用の資料の方が参考にしやすいかもしれません。PGConf.ASIA 2017用の資料は英語(と日本語訳)でまとめていますが、PostgreSQL Conference Japan 2017用の資料は日本語でまとめているからです。
PGroongaを使うと全文検索システムのバックエンドとしてもPostgreSQLを活用できます。ぜひ活用してください!
PGConf.ASIA 2017で、先日リリースしたPGroonga 2を紹介しました。PGroongaも使ってPostgreSQLをどんどん活用してください!もし、PGroonga関連でなにか相談したいことがある場合はお問い合わせください。
企業利用においては、Firefox/Thunderbirdの特定の機能を使わせないようにカスタマイズしたいという場合があります。
そのための機能を提供するアドオンにglobalChrome.css
があります。
(使用例は過去のThunderbirdのインターフェースをカスタマイズするにはという記事をご参照下さい。)
しかし、WebExtensions APIへの移行の流れにともない、Firefox ESR59以降ではglobalChrome.css
は使えないことが決まっています。また、Firefox Quantum以降ではすでにアドオンの互換性がなくなっています。
今回は、globalChrome.css
で行っていたカスタマイズを別の手段で代替する方法と、その注意点をご紹介します。
Firefox/Thunderbirdは、アプリケーション自体のUIがXMLとCSSで構成されています。 このアドオンは、Firefox/ThunderbirdのUIに追加のスタイルシートを反映することで、その外観を変化させるというアドオンです。 冒頭で紹介したように、企業利用においては不要な機能をあらかじめ削除するなど、ユーザーインターフェースをその企業に合わせてカスタマイズした状態で利用したい場合に使われます。
しかしながら、前述の通りこのアドオンはFirefox Quantum以降では動作しません。 これを使ってFirefox/Thunderbirdをカスタマイズしている場合、代替手段が必要です。 それがユーザースタイルシートと呼ばれるものです。*1
ユーザースタイルシートには次の2つがあります。
globalChrome.css
はこのうちuserChrome.css
にて代替することができます。
ユーザースタイルシートといわれるのがこれら2つのファイルですが、違いはなんでしょうか。 Mozilla のカスタマイズには、次のような説明があります。
これらのファイルの違いをuserChrome.cssとuserContent.cssのサンプル設定から示します。
userChrome.cssには文字色を赤にする全称セレクタの設定を適用してみます。
*|* {
color: red !important;
}
userContent.cssには文字色を青にする全称セレクタの設定を適用してみます。
*|* {
color: blue !important;
}
すると、UIクローム部分は赤い文字に、コンテンツ領域は青い文字になります。 この事から、それぞれのユーザースタイルシートがFirefox/Thunderbirdのどの部分に作用するかが分かります。
globalChrome.css
はuserChrome.css
にて代替することができると述べましたが、両者には1点大きな違いがあります。展開のタイミングです。
globalChrome.css
は通常カスタマイズ済みFirefoxのインストーラとともにバンドルされます。そのためインストール時に所定の場所に配置されます。
しかし、userChrome.css
はユーザーのプロファイルディレクトリに配置しなければいけません。
プロファイルが作成されるのは、インストール後の起動時ですので配置すべきタイミングが異なります。
css | 配置先(Windowsの場合) |
---|---|
globalChrome.css | C:\Program Files (x86)\Mozilla Firefox\chrome\globalChrome.css |
userChrome.css | %APPDATA%\Mozilla\Firefox\Profiles(プロファイルフォルダ)\chrome\userChrome.css |
したがって、プロファイル作成後にuserChrome.css
を配置する仕組みを構築する必要があります。*2
今回は、globalChrome.css
が来たるべきFirefox ESR59以降では使えなくなることにともない、その移行のポイントについて紹介しました。
FirefoxやThunderbirdの導入やカスタマイズでお困りで、自力での解決が難しいという場合には、有償サポート窓口までぜひ一度ご相談下さい。
あっという間に一週間が経ってしまう須藤です。
一週間ほど前の2017年12月05日にドリコムさんでOSS Gate東京ふりかえり2017-12を開催しました。
これはよりうまくOSS Gateに取り組むための活動です。最近の4ヶ月で得られた知見を活用して、次の4ヶ月はもっとうまく活動します。去年は1年に1回だけ実施しましたが、それだとフィードバックの間隔が開きすぎて得られた知見を活かしきれないという知見が得られました。そのため、今年は4ヶ月に1回の開催にチャレンジしています。(去年よりも今年の方がうまく活動できるようにしている。)
今回は今年最後の開催でした。これまでは、事前にオンラインで知見を集めて、東京でオフラインで集まる、というやり方でしたが、今回は東京と大阪をビデオ会議システムでつないで開催しました。(今回のチャレンジ。)東京と大阪ではOSS Gateへの取り組み方が少し異なるので、違うやり方で得られた知見をお互いに共有して次に活かせることを期待していました。
実際にやってみた結果ですが、東京と大阪をつないでよかったです。今までにはない発想を得られました。1つ例を紹介します。
東京ではワークショップのビギナー(OSSの開発に参加したいけど未経験の人)とサポーター(ビギナーをサポートする人)の割合のバランスが悪いという課題があります。ビギナーは口コミで毎回たくさん新しい人が集まるけど、サポーターはなかなか新しい人が増えていません。そのため、サポーターが不足しがちです。
これに対し、東京ではサポーター1人でいかに多くのビギナーをサポートするかという方向でがんばっていました。一方、大阪ではビギナーとサポーターが1:1くらいになるようにビギナーの参加人数を絞っていました。大阪では「サポーターの負担が少ない体制を維持した方が新しくサポーターに入ろうとする人の敷居が下がり、結果としてサポーターが増えて対応できるビギナーも多くできるのではないか」という考えでした。
東京ではこのような発想はまったく思い浮かばなかったので非常によい機会になったと思っています。
これは東京が大阪の知見を学んだ例ですが、逆に大阪が東京の知見を学んだ例もあり、実によい機会でした。これからも続けていくことにしました。
前置きはこんな感じで、参加できなかった人たちのために、実際にどのようなことが決まったかをざっくりとまとめておきます。
東京も大阪と同じようにビギナーとサポーターの比率を1:1に維持するようにビギナーの参加を制限します。
現時点で次回のワークショップである、来年01月27日開催のOSS Gate東京ワークショップ2018-01-27はビギナーが8人キャンセル待ちになっています。サポーターが増えるとビギナーの定員を増やせるので、OSS開発の経験者(過去にビギナーだった人とか)はぜひサポーターとして参加してください!
東京のワークショップは10:30開始で、大阪は13:00開始です。東京でも13:00開始の方が参加しやすい人が多いかもしれないので13:00開始を試します。
ただし、もともと10:30開始(というより17:00までに終了)の方がうれしい人が多かったから10:30開始にしていたので、10:30開始は引き続き継続します。
この10:30開始と13:00開始を両立するために次のような開催スケジュールにします。
1月と4月と7月と10月は最終土曜日に10:30から17:00で開催
3月と6月と9月と12月は第2土曜日に13:00から19:00で開催
これまでは奇数月の最終土曜日に開催していましたが、1.5ヶ月毎に開始時間を交互にしながら開催します。
ワークショップ後に懇親会を開催します。
これは、懇親会を開催した方が継続してサポーター参加する人が増えるのではないかという仮説を検証するためです。
懇親会は13:00-19:00開催のときに実施します。
大阪では毎回ワークショップの進行役を変えているそうなので、東京でも真似をします。
もともと東京でも進行役経験者を増やしたかったので、増やすように取り組んでいたのですが、大阪のように「毎回新しい人」ほどは取り組んでいませんでした。大阪ではビギナーとサポーターの比率が1:1なのでまわりのサポーターがフォローしやすく、初めての人でも進行役をやりやすいのだそうです。東京でもそのような環境を整備して、大阪のように取り組みます。
とりあえず、「過去にサポーターとして参加したときのアンケートに進行役できそうと答えた進行役未経験のサポーターのうち、最初に登録した人」というルールで進行役を選んでやってみる予定です。
大阪では毎回ワークショップの会場を変えているそうです。
会場提供という形でOSS Gateに関わる人が増えるのはOSS Gateとしてはよいことのように思えるので、東京でも真似します。
これまではクラウドワークスさんに会場を提供してもらうことが多く、非常に助かってしました。非常に助かるので、10:30開催のときは引き続きお願いする予定です。
一方、13:00開催の方は毎回違う会場にする予定です。こっちは懇親会もやるので、いろんな場所で開催できた方が都合がよさそうだからです。
なお、次の13:00開催のOSS Gate東京ワークショップ2018-03-10の会場はまだ決まっていません。会場を提供できる!という方はGitterのoss-gate/tokyoチャンネルで宣言するか、@ktouに直接教えてください。
東京はOSS開発未経験の人の最初の一歩を支援するワークショップだけでなく、二歩目以降を支援するミートアップという取り組みや、対象OSSを限定した特化型のワークショップ・ミートアップも試しています。
大阪は現在はワークショップのみ取り組んでいますが、うまくやるこつが掴めてきたので、ワークショップ以外の取り組みを始めてみます。具体的には、来年の3月にGitLabに限定した取り組みを試してみます。
OSS Gateはこんな感じで普通の活動の中に「よりよく活動するための活動」を含めています。
OSS Gateのイベント(ワークショップやミートアップなど)に参加したことがある人はぜひふりかえりにも参加してみてください。ワークショップやミートアップで学べることとはまた違ったことが学べますよ。
東京にいる元ビギナーの人は来年1月末のOSS Gate東京ワークショップ2018-01-27にサポーターとして参加しにきてね!!!
Firefoxの法人向けサポートにおいては、クラッシュした際のクラッシュレポートファイルを提供してもらって、問題の切り分けをするということがあります。
今回は、そのような場合に有用なFirefoxのクラッシュレポートを解析する方法について紹介します。
Mozillaが提供しているクラッシュレポーターの解析サイトとしてMozilla Crash Reportsがあります。
Firefoxがクラッシュしたときに起動するクラッシュレポーターによるクラッシュレポートの送信先であり、クラッシュレポートの統計情報を閲覧することもできるサイトです。
クラッシュレポートを解析した結果をブラウザ経由で閲覧できるのでおすすめです。
実際にクラッシュレポートを送信するにはいくつか準備が必要です。
前提条件として、クラッシュした端末とは別の端末からクラッシュレポートを送信するものとします。
クラッシュレポートに関する詳細はMozilla クラッシュレポーターにて解説されていますが、以下のように所定の pending
ディレクトリ配下にクラッシュレポートファイルを配置します。
対象ファイル | 配置先(Windowsの場合) |
---|---|
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.dmp | %APPDATA%\Mozilla\Firefox\Crash Reports\pending\xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.dmp |
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.extra | %APPDATA%\Mozilla\Firefox\Crash Reports\pending\xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.extra |
配置できたら、Firefoxを起動し、 about:crashes
をロケーションバーに入力します。
すると、配置したファイルが未送信のクラッシュレポートとして認識されます。
認識されている未送信のクラッシュレポートのレポートIDをクリックすると、クラッシュレポートを送信することができます。 送信が完了すると、送信済みクラッシュレポートとして認識されます。
送信済みクラッシュレポートのレポートをIDをクリックすると、実際の解析結果を参照することができます。
今回は、Firefoxのクラッシュレポートを解析する方法について紹介しました。 実際にクラッシュレポートの内容をどう活用するかについては、別の記事にて紹介します。
Firefoxの導入時の要件として、クラッシュ時のレポートを送信しないようにするという設定を行う事があります。 この設定が意図通りに反映されているかどうかを確認するために、Firefoxが実際にクラッシュした時の様子を観察したい場合があります。
Firefoxには既知で且つ未修正のクラッシュバグがいくつかあるため、それを突くようなコードを実行すればFirefoxをクラッシュさせる事ができます。しかし、クラッシュバグは再現性が低い物もありますし、そもそも新しいバージョンのFirefoxではクラッシュしないように修正されていることも多いです。安定してFirefoxをクラッシュさせる方法を把握しておけば、Firefoxをクラッシュさせるための方法をその都度あれこれ調べなくても済みます。
以前、Firefoxを意図的にクラッシュさせる方法として js-ctypes
を利用してFirefoxをクラッシュさせる方法を紹介しました。今回はその記事の更新版としてより簡単な方法を紹介します。
Firefox 55以降では、意図的にクラッシュさせる方法が用意されています。それが Components.utils.crashIfNotInAutomation
です。
実際にこれを使ってクラッシュさせるには次の手順を踏みます。
about:config
を開き、devtools.chrome.enabled
の値を true
に設定する。これでFirefoxを実際にクラッシュさせることができます。
今回は、Firefoxを意図的にクラッシュさせる最新の方法を紹介しました。 Firefox 55以降で導入された仕組みのため、それ以前のFirefoxではFirefoxを意図的にクラッシュさせる方法で紹介した方法がよいでしょう。*1
*1 js-ctypesを使った方法はFirefox 57でも依然として有効です。
今年もAdvent Calendarの季節になりましたね。
Debsources now in sources.debian.orgと題したメールにて、https://sources.debian.org/
というサイトの公開がアナウンスされました。
従来 sources.debian.net
ドメインで稼働していたものが、Debian公式として提供されるようになりました。
Debian Sourcesについては、さっそく解説している記事もあります。
今回は、Debian Sourcesにて提供されているAPIの使用例として、よく使われているパッケージにどれくらいパッチが当てられているのかを調べてみることにします。
API に関しては API Documentation があるのでそちらを参考にしました。APIにアクセスするために特別な認証は必要ありません。
パッケージリストについては https://sources.debian.org/api/list
からJSONのレスポンスを取得することができます。
prefix
つきでパッケージリストを取得することもできるようです。
よく使われているパッケージについては、 Debian Popularity Contestの結果を利用しました。
例えば、 main
カテゴリなら https://popcon.debian.org/main/by_inst
にアクセスするとどれだけインストールされたかという値を取得できます。
今回は取得したパッケージリストをすべて調べ上げることはせず、10000回以上インストールされているパッケージのみをフィルタする目的で使いました。
パッチファイルのリストは https://sources.debian.org/patches/api/(パッケージ名)/latest
でJSONのレスポンスを取得できます。
例えば、 systemd
のパッチリストを取得するなら https://sources.debian.org/patches/api/systemd/latest
にアクセスします。
2017年12月18日現在のパッチ適用状況を元にしています。
main
カテゴリのうち、popconで10000回以上インストールされており、パッチの適用数が多いもの上位50件リストアップした結果は以下のとおりです。
No. | Package | Patch |
---|---|---|
1 | systemd | 59 |
2 | imagemagick | 56 |
3 | libxml2 | 55 |
4 | php5 | 52 |
5 | gnupg2 | 51 |
6 | cups | 50 |
7 | bash | 50 |
8 | php7.0 | 47 |
9 | mutt | 46 |
10 | perl | 46 |
11 | thunderbird | 42 |
12 | python3.5 | 38 |
13 | w3m | 37 |
14 | ppp | 36 |
15 | python3.6 | 35 |
16 | firefox-esr | 33 |
17 | libreoffice | 31 |
18 | iceweasel | 31 |
19 | openssl | 30 |
20 | procmail | 30 |
21 | wireshark | 28 |
22 | bsd-mailx | 27 |
23 | ispell | 26 |
24 | fortune-mod | 26 |
25 | cinnamon | 25 |
26 | texlive-base | 23 |
27 | festival | 22 |
28 | ntp | 21 |
29 | network-manager | 20 |
30 | pm-utils | 20 |
31 | blt | 20 |
32 | util-linux | 20 |
33 | transfig | 19 |
34 | groff | 19 |
35 | sysvinit | 18 |
36 | ruby1.9.1 | 17 |
37 | cpio | 17 |
38 | samba | 17 |
39 | pulseaudio | 17 |
40 | rpm | 17 |
41 | open-vm-tools | 17 |
42 | parted | 16 |
43 | arj | 16 |
44 | nodejs | 16 |
45 | clamav | 16 |
46 | xsane | 16 |
47 | tmux | 15 |
48 | libsoftware-license-perl | 15 |
49 | exim4 | 15 |
50 | tcpdump | 15 |
今回は、Debian Sources
というサイトのAPIの使い方を紹介しました。
単純なAPIですが、他の情報と組み合わせると「よく使われているパッケージのうち、パッチの適用数が多いもの」をリストアップしてみたりすることもできます。
この記事を書いている時点では、まだパッチに関してDEP3の情報をレスポンスに含めるようにはなっていないようです。*1
DEP3の情報が含まれていると、「Forwarded」という「このパッチはアップストリームに報告されているか?」といった情報を調べて、まだ報告されていないなら報告しよう、といったことがやりやすくなります。*2
今後そういった情報も含まれるともっと活用の幅が広がるかもしれません。
Firefoxの法人向けサポートにおいては、クラッシュした際のクラッシュレポートファイルを提供してもらって、問題の切り分けをするということがあります。
以前、そのような場合に有用なFirefoxのクラッシュレポートを解析する方法をFirefoxのクラッシュレポートを解析するにはという記事で紹介しました。
今回は、実際にクラッシュレポートの解析結果をどのように読み解いたらよいのかを説明します。
前回の記事ではクラッシュレポートを送信してMozilla Crash Reportsにて確認するところまでを紹介しました。
送信したクラッシュレポートの解析結果は次のようにカテゴリに分けてタブで表示されます。
カテゴリ | 説明 |
---|---|
Details | クラッシュレポートのスタックトレースを表示 |
Metadata | 端末の搭載メモリなどの情報を表示 |
Bugzilla | スタックトレースから自動検出された関連するバグのリストを表示 |
Modules | 名前解決に必要なシンボル(.pdb)ファイルのリストを表示 |
Raw Dump | クラッシュした際のダンプデータをテキスト形式で表示 |
Extensions | クラッシュした際にインストールされていたアドオンのリストを表示 |
Telemetry Environment | アドオンやプラグインの設定情報を表示 |
着目するポイントは Details
と Bugzilla
です。
Details
にはクラッシュレポートのスタックトレースが表示されます。
上記の例だと、 Frame 0
にて xul.dll
の nsINode::GetAsText()
でクラッシュしたことがわかります。
また、Source
カラムからクラッシュした該当行のソースコードも知ることができます。
次に着目するのは、 Bugzilla
です。スタックトレースから自動的に検出された関連しそうなバグを表示してくれます。
場合によっては、ここで提示されたバグの内容と実際にクラッシュした原因が一致し、問題の特定につながることがあります。
クラッシュした内容が既知のバグであれば、どのような状況でクラッシュするのかの補足情報が得られます。 すでに修正済みであればどのバージョンでその修正がリリースされるのか、あるいはバックポートされないのかという情報も得られます。
今回は、実際にクラッシュレポートの解析結果をどのように読み解いたらよいのかを説明しました。
Details
と Bugzilla
の内容を確認するだけでも問題解決には有用です。
FirefoxやThunderbirdの導入やカスタマイズ、クラッシュなどの現象でお困りで、自力での解決が難しいという場合には、有償サポート窓口までぜひ一度ご相談下さい。
WebExtensions APIに基づくFirefox用アドオンでは、ユーザーインターフェースを提供するための方法として、ツールバーボタンのポップアップメニュー、サイドバーといったUIの部品として表示される物以外に、通常のタブやウィンドウとして開くためのページを組み込む事ができます。
といっても実現方法は非常に単純で、開きたいページを実装したHTMLファイルをアドオンのパッケージ内に含めた上でbrowser.tabs.create({ url: "./group-tab.html" })
のように指定してタブで開くだけです。実際に、例えばツリー型タブでは、初回インストール時などに開かれる説明ページや、ブックマークフォルダからまとめてタブを開いた時などにそれらをグループ化するために使われるタブ(以下、「グループタブ」と呼ぶ事にします)がこの方法で実装されています。
さて、この方法で開かれたタブを一種のUIとして使う場合に、1つ気をつけなくてはならない事があります。それは、ページの作り方によっては、Firefoxの再起動時に必ずそのタブが失われてしまう場合があるという事です。再起動以外にも、アドオンマネージャでアドオンを一時的に無効化した時や、アドオンが自動更新された時なども同じ事が起こります。本項で述べる条件に当てはまるページを開いているタブは、これらの場面でFirefoxによって勝手に閉じられてしまうという性質があります。
その条件とは、端的に言えば、ページの一部として、アドオンの通常の権限で実行可能なJavaScriptを含むページです。具体的には、<script>
タグの内容として直接スクリプトを記述している場合*1や、パッケージ内に含まれるJavaScriptのファイルを<script type="application/javascript" src="./group-tab.js"></script>
のようにして参照している場合がこれにあたります。
ユーザーの操作に反応するなどの動的な処理を行うためには、スクリプトの使用は避けて通れません。実際に「ツリー型タブ」のグループタブでも、タブ名の部分をクリックして編集したり、「一時的なグループ」チェックボックスの状態をURLのクエリパラメータとして保持したりするために、スクリプトを使う必要があります。しかし上記の理由から、そのままだとFirefoxの再起動やアドオン自体の更新の度にグループタブが失われてしまうという事になります。グループタブはタブのツリーを形成する要素の1つなので、勝手に失われてしまうとツリーが壊れてしまいますから、これでは実用に耐えません。
どうすればこの問題を解消できるでしょうか?
タブが閉じられてしまう原因は、前述した通り「アドオンの通常の権限を持ったスクリプトがそのページ内で動作している」せいです。言い換えると、そうでないスクリプトが実行されているだけであれば、タブが勝手に閉じられてしまう事はありません。
「そうでないスクリプト」とは何かというと、コンテントスクリプトがそれにあたります。
コンテントスクリプトは、Webページの名前空間にアドオンから任意のスクリプトを注入して実行する仕組みです。注入するスクリプトの中では一般的なJavaScriptの機能の他にWebExtensions APIのサブセットを利用できます。これらの範囲内だけでページの機能を実装すれば、「ツリー型タブ」のグループタブのように「Firefoxを再起動したりアドオンを更新したりしても勝手に閉じられない、機能性を持ったタブ」を開いておけます。
コンテントスクリプトは一般的には、manifest.json
のcontent_scripts
キーを使って注入の指示を静的に・宣言的に指定します。しかし、今回の用途にはこの方法は使えません。
アドオンのパッケージ内に含まれるHTMLファイルは、実際にはmoz-extension://a5abe0a8-70d1-4c64-975b-b19c7f7740fe/resources/group-tab.html
のような内部的なURLで参照されています。content_scripts
でのコンテントスクリプトの注入対象はマッチパターンで指定する必要があるのですが、実は、この内部的なURLに対してはどんなマッチングパターンを指定しても期待通りの結果を得られない(指定したコンテントスクリプトが読み込まれない)のです。例えば以下の要領です。
"content_scripts": [
{
"matches": [
/* moz-extension:を含むマッチングパターンは不正な物として扱われる */
"moz-extension://*/group-tab.html*",
/* かといって、こちらも期待通りにマッチしない */
"<all_urls>"
],
"run_at": "document_start",
"js": [
"/group-tab.js"
]
}
],
そもそも、上記の例のようにアドオンの内部的なURLはUUIDを含む形になっており、そのUUIDがインストールする度に変わるようになっている*2ことから、静的・宣言的な定義ではURLを正確に指定できないという問題もあります。無駄に広範なマッチングパターンを設定して無関係のページにまでスクリプトを注入してしまうというのは、褒められた事ではありません。
今回のような場面では、コンテントスクリプトを動的に読み込む方法を使う必要があります。
tabs.executeScript()
は、特定のタブに任意のタイミングでコンテントスクリプトを注入する機能です。一般的なWebページに対してはマッチパターンであらかじめ許可を与えられた場合にのみ使えるのですが、例外として、そのアドオン自身に含まれているページ(上記の内部的なURLで示されるページ)に対しては事前の許可無しで使えるようになっています。以下はこれを使って、アドオンに含まれるページがタブに読み込まれた時点でそれを検知し、コンテントスクリプトを注入する例です。
// "tabs" をpermissionsに含めておく必要がある。
// スクリプトを注入したいページが新たに読み込まれた時のハンドリング。
browser.tabs.onUpdated.addListener((aTabId, aChangeInfo, aTab) => {
if (aChangeInfo.status || aChangeInfo.url)
tryInitGroupTab(aTab);
});
// スクリプトを注入したいページを既に読み込み済みのタブがアクティブになった時のハンドリング。
browser.tabs.onActivated.addListener(async (aActiveInfo) => {
var tab = await browser.tabs.get(aActiveInfo.tabId);
tryInitGroupTab(tab);
});
// 事前にUUIDを含む内部URLを特定しておく。
const GROUP_TAB_URL = browser.extension.getURL('/group-tab.html');
async function tryInitGroupTab(aTab) {
// URLをヒントに、スクリプトを注入したいページを開いたタブかどうかを判別する
if (aTab.url.indexOf(GROUP_TAB_URL) != 0)
return;
browser.tabs.executeScript(aTab.id, {
runAt: 'document_start',
matchAboutBlank: true
file: '/group-tab.js'
});
}
実際にリリースされているバージョンの「ツリー型タブ」でも、これと同様の事を行ってグループタブの挙動を実現しています。
コンテントスクリプトではWebExtensions APIのサブセットのみ使えますが、その中にはbrowser.runtime.sendMessage()
が含まれています。サブセットに含まれていないAPIが必要な機能をそのページ起点で使いたい場合は、必要なAPIを自由に使えるバックグラウンドスクリプトなどで機能の大部分を実装しておき、browser.runtime.sendMessage()
でそれを呼び出すという形にすると良いでしょう。
ここからは余談として、「アドオンの通常の権限で実行可能なJavaScriptを含むページを読み込んでいるタブがFirefoxによって勝手に閉じられてしまう」という事について、実際に起こっていた現象からその挙動の元になっている実装を特定するまでの調査の様子をご紹介します。
調査は主に、Firefoxのソースコードをオンラインで検索できるDXRで行いました。まずWebExtensions関係の実装が含まれているディレクトリ配下で(path:components/extensions
)、自動テストのファイルと思われるファイル以外で(-path:test
)、タブを閉じるための内部的なメソッドを参照している箇所(removeTab
)を検索しました。すると、WebExtensions関係でタブを閉じる処理を行っていると思われる箇所が数カ所見つかります。この中でメソッド名の部分一致でない検索結果は以下の2箇所でした。
前者はbrowser.tabs.remove()
の挙動を実装している箇所なので、今回の挙動とは無関係である可能性があります。その一方で、後者はアドオンの無効化時やFirefoxの終了時などのシャットダウン処理にフックを仕掛けて、一定の条件が満たされた時にタブを閉じるという実装になっています。起こっている現象から見て、こちらの箇所が問題の挙動の原因である可能性が高そうです。
そこで後者のコードでフックを仕掛けているアドオンのシャットダウン処理の通知の出所を検索してみたところ、ExtensionPageContextParent
というクラスのshutdown
というインスタンスメソッドの中から通知されている事が分かりました。この時、通知のメッセージと共にExtensionPageContextParent
のインスタンス自身がリスナに渡されており、そこから芋蔓的に「閉じられるべきタブ」が特定されているという事も分かりました。ということは、このインスタンスがどこで作成されているのかを調べれば、タブが勝手に閉じられてしまう条件が掴めそうに思えます。
という事でクラス名で再検索すると、ExtensionPageContextParent
クラスのcreateProxyContext
メソッドの中でインスタンスが作られていて、このメソッドはe10sにおけるプロセス間通信でのAPI:CreateProxyContext
というメッセージを切っ掛けに実行されている事が分かりました。このメッセージの出所はChildAPIManager
クラスのコンストラクタの中だという事も分かりました。
このクラスのインスタンスが作られる場面を検索すると、以下の3箇所が該当しました。
ここでそれぞれのコードの周囲を見ると、先程見たExtensionPageContextParent
のインスタンスが作られる分岐に入る条件に現れている"addon_parent"
という文字列と同じ物が、2番目のアドオンに含まれるページの名前空間の初期化処理らしき箇所にもある事と、コンテントスクリプトの名前空間の初期化処理らしき箇所からは別の分岐に流れている様子が窺えました。
以上の調査結果と、実際の検証時の「ページに埋め込んだスクリプトやページから直接参照したスクリプトがある時はタブが閉じられて、コンテントスクリプトを注入しただけだとタブは閉じられない」という結果から、スクリプトの名前空間が破棄される時に、そのページが読み込まれているタブを自動的に閉じるコードが実行されうるのは、「コンテントスクリプト」「開発ツールのスクリプト」以外の全般的な「アドオンに含まれるページのスクリプト」だけであるようだと判断しました。
Firefox用のアドオンにHTMLとJavaScriptで実装されたページを含めるにあたって、そのページを開いたタブがFirefoxによって勝手に閉じられてしまう場合があるという事と、その条件、回避方法を解説しました。また、条件を特定するにあたって具体的に行った調査の進め方の例もご紹介しました。
OSS・フリーソフトウェアの開発時やAPIの挙動に不可解な部分があって、ドキュメントにそれらしい解説が見つからない場合、それはまだドキュメント化されていない仕様に基づく物である可能性があります。そういう時には、せっかくソースを読める状況にあるのですから、全くの当てずっぽうで使うのではなく、その挙動の原因を明らかにしてから使うようにしてみてはどうでしょうか。そうすることで、最終的なプロダクトの挙動に対して、より確かな自信を持つ事ができるようになるかもしれません。皆さんも、実際に動作しているOSS・フリーソフトウェアのソースをぜひ見てみて下さい。
*1 <script>
タグの内容にスクリプトを直接書いた物(インラインスクリプト)は、アドオンにおいては安全のため初期状態では実行されません。実行を許可するためには、実行したいスクリプトのハッシュ値をecho '<script>タグの内容' | openssl dgst -sha256 -binary | openssl enc -base64
などの方法で求めて、マニフェストファイルのcontent_security_policyキー
を使ってインラインスクリプト用のContents Security Policyを設定する必要があります。
*2 これは、Webページがアドオンの内部URLを参照して各ユーザーのアドオンのインストール状況を調べユーザーの動向をトラッキングする「フィンガープリンティング」を防止するための仕様です。
ソフトウェアをアンインストールする際には、ゴミや痕跡を無駄に残さない事が望ましいです。また、イベントを監視する必要のある機能を含んでいる場合、監視の必要がなくなったにも関わらず監視を続けていると、メモリやCPUを無駄に消費する事になります。こういった無駄を取り除くために行うのが、いわゆる終了処理です。Firefoxのアドオンでも、場合によって終了処理が必要になってきます。
WebExtensions APIはGoogle Chromeの拡張機能向けAPIのインターフェースを踏襲しており、その中には、アドオンがアンインストールされたり無効化されたりしたタイミングで実行されるイベントハンドラを定義するための仕組みも含まれています。以下の2つがそれです。
しかしながら、これらのAPIはFirefox 57の時点で未実装のため、Firefoxのアドオンでは使用できません。よって、これらのタイミングでの終了処理で後始末をしなければならない類のデータについては、FAQやアドオンの紹介ページの中で手動操作での後始末の手順を案内したり、あるいはそれを支援するスクリプトなどを配布したりする必要があります。
ただ、データの保存の仕方によっては終了処理がそもそも必要ない場合もあります。具体的には、browser.storage.local
を使用して保存されたデータがこれにあたります。browser.storage.local
の機能で保存されたデータはアドオンのアンインストールと同時にFirefoxによって削除されますので、アドオン側でこれを消去する終了処理を用意する必要はありません。
ツールバーのボタンのクリックで開かれるポップアップパネル内や、サイドバー内に読み込んだページにおいて登録されたイベントリスナーは、それらのページが破棄されるタイミングで動作しなくなる事が期待されます。そのため、これらのページでは特に終了処理は必要ない場合が多いです。
しかしながら、これらのページだけで完結せず、バックグラウンドページやコンテントスクリプトと連携する形で機能が実装されている場合には終了処理が依然として必要です。
例えば、ツリー型タブはツールバーボタンのクリック操作でサイドバーの表示・非表示をトグルできるようになっていますが、この機能はサイドバーとバックグラウンドページの連携によって実現されています。というのも、サイドバーの表示・非表示を切り替えるAPIはユーザーの操作に対して同期的に実行された場合にのみ機能して、それ以外の場合はエラーになる、という制限があるからです。WebExtensionsには今のところサイドバーの開閉状態を同期的に取得するAPIがありません。また、ツールバーボタンの動作を定義する箇所で開閉状態のフラグをON/OFFしても、サイドバーのクローズボックスや他のサイドバーパネルの切り替え操作など、ツールバーボタンのクリック操作以外にもサイドバーパネルが開閉される場面は数多くあるため、フラグと実際の状態がすぐに一致しなくなってしまいます。そのため、サイドバー内のページの初期化処理中にバックグラウンドページに対してbrowser.runtime.sendMessage()
で通知を送り、サイドバーが開かれた事をフラグで保持し、ツールバーボタンの動作において同期的にフラグを参照しているわけです。
サイドバーが開かれた事はこれで把握できますが、問題はサイドバーが閉じられた事の把握です。ここで「サイドバー内のページのための終了処理」が必要となります。
ページが閉じられた事を検知する最も一般的な方法は、ページが破棄される時に発行されるDOMイベントを捕捉するという物です。このような用途に使えそうなDOMイベントは以下の4つがあります。
close
beforeunload
unload
pagehide
この中で、サイドバーやポップアップに表示されるページにおいてclose
は通知されず、実際に使えるのは残りの3つだけです。よって、これらの中のいずれかを捕捉して以下のように終了処理を行う事になります。
window.addEventListener('pagehide', () => {
...
// 何らかの終了処理
...
}, { once: true });
ただし、このタイミングでできる終了処理は非常に限定的です。例えば、browser.runtime.sendMessage()
でバックグラウンドページ側にメッセージを送信しようとしても、そのメッセージが通知されるよりも前にスクリプトの名前空間が破棄されてしまうせいか、実際にはそのメッセージがバックグラウンドページ側に通知される事はありません。ツリー型タブの事例だと、このタイミングで「サイドバーが閉じられた(ページが破棄された)」というメッセージをバックグラウンドページに送ろうとしても、そのメッセージは実際には届く事は無いため、バックグラウンドページから見るとサイドバーは開かれたままとして認識されてしまう事になります。
DOMイベントのリスナーではできない終了処理をする方法として、バックグラウンドページとそれ以外のページの間で接続を維持しておき、その切断をもってページが閉じられた事を検出するというやり方があります。
browser.runtime.connect()
は、バックグラウンドページとサイドバー内のページのような、異なる名前空間のスクリプト同士の間で双方向にメッセージを送受信できる専用の通信チャンネル(runtime.Port
)を確立するAPIです。browser.runtime.sendMessage()
で送信したメッセージはbrowser.runtime.onMessage
にリスナを登録しているすべてのスクリプトに通知されますが、この方法で確立した通信チャンネル上を流れるメッセージは、接続を要求した側と受け付けた側のお互いにのみ通知されるという違いがあります。
このAPIは双方向通信のための仕組みなのですが、確立した通信チャンネル(runtime.Port
)のonDisconnect
にリスナを登録しておくと、接続元のページが閉じられたなどの何らかの理由で接続が切れたという事を、接続を受け付けた側で検知できるという特徴があります。これを使い、サイドバー内に開かれたページからバックグラウンドページに対して接続を行って、バックグラウンドページ側で接続の切断を監視すれば、間接的にサイドバー内に開かれたページが閉じられた事を検知できるという訳です。以下は、その実装例です。
var gPageOpenState = new Map();
var CONNECTION_FOR_WINDOW_PREFIX = /^connection-for-window-/;
browser.runtime.onConnect.addListener(aPort => {
// サイドバー内のページからの接続を検知して処理を行う
if (!CONNECTION_FOR_WINDOW_PREFIX.test(aPort.name))
return;
// 接続名に含めた、サイドバーの親ウィンドウのIDを取り出す
var windowId = parseInt(aPort.name.replace(CONNECTION_FOR_WINDOW_PREFIX, ''));
// サイドバーが開かれている事を保持するフラグを立てる
// (以後は、このフラグを見ればそのウィンドウのサイドバーが開かれているかどうかが分かる)
gPageOpenState.set(windowId, true);
// 接続が切れたら、そのウィンドウのサイドバーは閉じられたものと判断し、フラグを下ろす
aPort.onDisconnect.addListener(aMessage => {
gPageOpenState.delete(windowId);
});
});
window.addEventListener('DOMContentLoaded', async () => {
// このサイドバーの親となっているウィンドウのIDを取得する
var windowId = (await browser.windows.getCurrent()).id;
// サイドバーが開かれた事をバックグラウンドページに通知するために接続する
browser.runtime.connect({ name: `connection-for-window-${windowId}` });
}, { once: true });
確立した通信チャンネルそのものは使っていない、という所がミソです。
browser.runtime.sendMessage()
で送出されたメッセージは、browser.runtime.onMessage
のリスナで受け取って任意の値をレスポンスとして返す事ができます。また、誰もメッセージを受け取らなかった場合(誰もレスポンスを返さなかった場合)には、メッセージの送出側にはundefined
が返されます。この仕組みを使い、バックグラウンドページから送ったメッセージにサイドバーやツールバーボタンのパネル側で応答するようにすると、そのページがまだ開かれているのか、それとも何らかの切っ掛けで閉じられた後なのかを判別できます。
async isSidebarOpenedInWindow(aWindowId) {
// サイドバーが開かれている事になっているウィンドウを対象に、死活確認のpingを送る
var response = await responses.push(browser.runtime.sendMessage({ type: 'ping', windowId: aWindowId }))
.catch(aError => null); // エラー発生時はサイドバーが既に閉じられていると見なす
// pongが返ってくればサイドバーは開かれている、有効な値が返ってこなければ閉じられていると判断する
return !!response;
}
var gWindowId;
window.addEventListener('DOMContentLoaded', async () => {
// このサイドバーの親となっているウィンドウのIDを取得する
gWindowId = (await browser.windows.getCurrent()).id;
}, { once: true });
browser.runtime.onMessage.addListener((aMessage, aSender) => {
switch (aMessage && aMessage.type) {
case 'ping':
// このウィンドウ宛のpingに対してpongを返す
if (aMessage.windowId == gWindowId) {
// Promiseを返すと、それがレスポンスとして呼び出し元に返される
return Promise.resolve(true);
}
break;
}
});
バックグラウンドページからポーリングすれば、前項の方法の代わりとして使う事もできますが、そうするメリットは特にありません。
Firefoxのアドオンにおいて、アドオン自体が使用できなくなる場面での終了処理は現状では不可能であるという事と、ツールバーボタンで開かれるパネルに読み込まれたページやサイドバーに読み込まれたページの終了処理の実現方法をご紹介しました。
WebExtensions APIは原則としてリッチなAPIセットを提供する事を志向しておらず、基本的な機能の組み合わせで目的を達成できるのであれば、リッチなAPIは実装しないという判断がなされる事が多いです。やりたい事をストレートに実現できるAPIが見つからない場合には、「APIが無いんじゃあ仕方がない」と諦めてしまわず、今あるAPIの組み合わせで実現する方法が無いか検討してみて下さい。
Firefoxのタブを参照するアドオンは、browser.tabs.get()
やbrowser.tabs.query()
などのAPIを使って各タブの状態を取得します。この時、Firefoxのタブの状態を表すオブジェクトはtabs.Tab
という形式のオブジェクトで返されます。
tabs.Tab
にはタブの状態を表すプロパティが多数存在していますが、ここに表れないタブの状態という物もあります。「未読」「複製された」「復元された」といった状態はその代表例です。これらはWebExtensions APIの通常の使い方では分からないタブの状態なのですが、若干の工夫で判別することができます。
タブの未読状態は、バックグラウンドのタブの中でページが再読み込みされたりページのタイトルが変化したりしたらそのタブは「未読」となり、タブがフォーカスされると「既読」となります。これは、tabs.onUpdated
でtitle
の変化を監視しつつ、tabs.onActivated
でタブの未読状態をキャンセルする、という方法で把握できます。以下はその実装例です。
// バックグラウンドページで実行しておく(tabsの権限が必要)
var gTabIsUnread = new Map();
browser.tabs.onUpdated.addListener((aTabId, aChangeInfo, aTab) => {
// アクティブでないタブのタイトルが変化したら未読にする
if ('title' in aChangeInfo && !aTab.active)
gTabIsUnread.set(aTabId, true);
});
browser.tabs.onActivated.addListener(aActiveInfo => {
// タブがアクティブになったら既読にする
gTabIsUnread.delete(aActiveInfo.tabId);
});
browser.tabs.onRemoved.addListener((aTabId, aRemoveInfo) => {
// タブが閉じられた後は未読状態を保持しない
gTabIsUnread.delete(aTabId);
});
// 上記の処理が動作していれば、以下のようにしてタブの未読状態を取得できる。
// var unread = gTabIsUnread.has(id);
上記の例ではMap
で状態を保持していますが、Firefox 57以降で使用可能なbrowser.sessions.setTabValue()
とbrowser.sessions.getTabValue()
を使えば、名前空間をまたいで状態を共有する事もできます。以下はその例です。
// バックグラウンドページで実行しておく(tabs, sessionsの権限が必要)
browser.tabs.onUpdated.addListener((aTabId, aChangeInfo, aTab) => {
// アクティブでないタブのタイトルが変化したら未読にする
if ('title' in aChangeInfo && !aTab.active)
browser.sessions.setTabValue(aTabId, 'unread', true);
});
browser.tabs.onActivated.addListener(aActiveInfo => {
// タブがアクティブになったら既読にする
browser.sessions.removeTabValue(aActiveInfo.tabId, 'unread');
});
// 上記の処理が動作していれば、以下のようにしてタブの未読状態を取得できる。
// var unread = await browser.sessions.getTabValue(id, 'unread');
WebExtensionsではタブが開かれた事をtabs.onCreated
で捕捉できますが、そのタブが既存のタブを複製した物なのか、閉じられたタブが復元された物なのか、それとも単純に新しく開かれたタブなのか、という情報はtabs.Tab
からは分かりません。しかし、タブのセッション情報を使えばこれらの3つの状態を判別できます。
複製や復元されたタブは、元のタブにbrowser.sessions.setTabValue()
で設定された情報を引き継ぎます。この性質を使うと、以下の理屈でタブの種類を判別できます。
browser.sessions.getTabValue()
でIDを取得してみて、取得に失敗したら(IDが保存されていなければ)そのタブは新しく開かれたタブである。(この判別方法にはFirefox 57以降で実装されたsessions
APIのbrowser.sessions.setTabValue()
とbrowser.sessions.getTabValue()
という2つのメソッドが必要となります。そのため、これらが実装される前のバージョンであるFirefox ESR52などではこの方法は使えません。また、これらのメソッドは今のところFirefoxでのみ実装されているため、GoogleChromeやOperaなどでもこの方法を使えないという事になります。)
以上の判別処理を実装すると、以下のようになります。
// バックグラウンドページで実行しておく(tabs, sessionsの権限が必要)
// IDからタブを引くためのMap
var gTabByPrivateId = new Map();
// 判別結果を保持するためのMap
var gTabType = new Map();
// 一意なIDを生成する(ここでは単に現在時刻とランダムな数字の組み合わせとした)
function createNewId() {
return `${Date.now()}-${parseInt(Math.random() * Math.pow(2, 16))}`;
}
// タブの種類を判別する
async function determineTabType(aTabId) {
// セッション情報に保存した独自のIDを取得する
var id = await browser.sessions.getTabValue(aTabId, 'id');
if (!id) {
// 独自のIDが保存されていなければ、そのタブは一般的な新しいタブであると分かるので
// 新たにIDを振り出す
id = createNewId();
// 振り出したIDをセッション情報に保存する
await browser.sessions.setTabValue(aTabId, 'id', id);
// IDでタブを引けるようにする
gTabByPrivateId.set(id, aTabId);
return { type: 'new', id };
}
// 独自のIDが保存されていれば、そのタブは複製されたタブか復元されたタブということになる
// そのIDをもつタブが存在するかどうかを調べる
let existingTabId = gTabByPrivateId.get(id);
// タブが存在しない場合、このタブは「閉じたタブを開き直す」またはセッションの復元で
// 開き直されたタブであると分かる
if (!existingTabId) {
gTabByPrivateId.set(id, aTabId);
return { type: 'restored', id };
}
// タブが存在していて、それが与えられたタブと同一である場合、
// この判別用メソッドが2回以上呼ばれたということになる
if (existingTabId == aTabId)
throw new Error('cannot detect type of already detected tab!');
// タブが存在しているが、与えられたタブではない場合、このタブは
// そのタブを複製したタブであると分かるので、新しいIDを振り出す
id = createNewId();
await browser.sessions.setTabValue(aTabId, 'id', id);
gTabByPrivateId.set(id, aTabId);
return { type: 'duplicated', id, originalId: existingTabId };
}
browser.tabs.onCreated.addListener(async (aTab) => {
// 新しく開かれたタブに対する任意の処理
// ...
// タブの種類の判別を開始する
var promisedType = determineTabType(aTab.id);
// 判別結果を他の箇所からも参照できるようにしておく
gTabType.set(aTab.id, promisedType);
var type = await promisedType;
// 上記判別結果を使った、新しく開かれたタブに対する任意の処理
// ...
});
browser.tabs.onRemoved.addListener(async (aTabId, aRemoveInfo) => {
// 削除されたタブに対する任意の処理
// ...
// それぞれのMapから閉じられたタブの情報を削除する
var type = await gTabType.get(aTabId);
gTabByPrivateId.delete(type.id);
gTabType.delete(aTabId);
});
// 既に開かれているタブについての初期化
browser.tabs.query({}).then(aTabs => {
for (let tab of aTabs) {
gTabType.set(tab, determineTabType(tab.id));
}
});
上記のようにしてtabs.onCreated
でタブの種類を判別してからその他の初期化処理を行う場合、タブの種類の判別は非同期に行われるため、tabs.onCreated
のリスナーが処理を終える前に他のイベントのリスナーが呼ばれる事もある、という点に注意が必要です。tabs.onUpdated
やtabs.onActivated
のリスナーが、tabs.onCreated
で何らかの初期化が行われている事を前提として実装されている場合、上記の判別処理やその他の非同期処理が原因で初期化が終わっていないタブが他のリスナーに処理されてしまうと、予想もしないトラブルが起こる可能性があります。
そのようなトラブルを防ぐためには、以下のようにしてタブの初期化処理の完了を待ってからその他のイベントを処理するようにすると良いでしょう。
var gInitializedTabs = new Map();
browser.tabs.onCreated.addListener(async (aTab) => {
var resolveInitialized;
gInitializedTabs.set(aTab.id, new Promise((aResolve, aReject) => {
resolveInitialized = aResolve;
});
// 任意の初期化処理
// ...
resolveInitialized();
});
// 別のウィンドウから移動されたタブに対してはtabs.onCreatedは発生しないため、
// tabs.onAttachedも監視する必要がある
browser.tabs.onAttached.addListener(async (aTabId, aAttachInfo) => {
var resolveInitialized;
gInitializedTabs.set(aTabId, new Promise((aResolve, aReject) => {
resolveInitialized = aResolve;
});
// 任意の初期化処理
// ...
resolveInitialized();
});
browser.tabs.onUpdated.addListener(async (aTabId, aChangeInfo, aTab) => {
await gInitializedTabs.get(aTabId);
// 以降、タブの状態の更新に対する任意の処理
// ...
});
browser.tabs.onActivated.addListener(async (aActiveInfo) => {
await gInitializedTabs.get(aActiveInfo.tabId);
// 以降、タブのフォーカス移動に対する任意の処理
// ...
});
// メッセージの処理
browser.runtime.onMessage.addListener((aMessage, aSender) => {
// この例では、必ずメッセージの`tabId`というプロパティでタブのIDが渡されてくるものと仮定する
if (aMessage.tabId) {
let initialized = gInitializedTabs.get(aMessage.tabId);
if (!initialized)
initialized = Promise.resolve();
// async-awaitではなく、Promiseのメソッドで初期化完了を待つ
// (関数全体をasyncにしてしまうと、このリスナが返した値が必ず
// メッセージの送出元に返されるようになってしまうため)
initialized.then(() => {
// 初期化済みのタブを参照しての何らかの処理
// ...
});
}
// その他の処理
// ...
});
// 他のアドオンからのメッセージの処理
browser.runtime.onExternalMessage.addListener((aMessage, aSender) => {
// ここでもbrowser.runtime.onMessageのリスナーと同じ事を行う
// ...
});
tabs.onUpdated
を監視する場合の、Bug 1398272への対策ここまでの実装例でtabs.onUpdated
を監視する例を示してきましたが、現時点での最新リリース版であるFirefox 57には、tabs.onUpdated
を監視しているとウィンドウをまたいでタブを移動した後にタブのIDの一貫性が損なわれる(本来であればウィンドウをまたいで移動した後もタブのIDは変わらない事が期待されるのに対し、このBugの影響により、ウィンドウをまたいで移動したタブに意図せず新しいIDが振り出されてしまう)という問題があります。
この問題を回避するには、IDの振り出し状況を監視して対応表を持つ必要があります。単純ではありますが、エッジケースの対応なども考慮に入れると煩雑ですので、この問題のWorkaroundとして必要な一通りの処理をまとめたwebextensions-lib-tab-id-fixer
というライブラリを作成・公開しました。tabs.onUpdated
を監視する必要があるアドオンを実装する場合には試してみて下さい。
WebExtensions APIで一般的には提供されていないタブの状態の情報について、既存APIの組み合わせで間接的に状態を判別する方法をご紹介しました。
現時点でFirefoxにのみ実装されているsessions
APIの機能には、このような意外な応用方法があります。皆さんも、今あるAPIを違った角度から眺めてみると、APIが無いからと諦めていた事について実現の余地が見つかるかもしれませんので、色々試してみる事をお薦めします
Lua用の使いやすいHTML・XML処理ライブラリーを開発しました。 これは、クリアコードが株式会社セナネットワークス様からの発注を受けて開発したライブラリーです。 XMLua(えっくすえむえるあ)といいます。MITライセンスで公開しています。
Luaは、スクリプト言語の書きやすさとC言語に匹敵する速さを持っている言語です。 速度は欲しいがC言語を使っての開発が大変というような状況のときによく使われます。 XMLuaは速度を大事にしているので、Luaが使われるような高速な動作が必要な状況でHTML・XMLを処理したいという場面で有用です。
現状は、最低限必要と思われる機能しか実装していませんが、XMLuaはLuaでHTML・XMLを処理したいという人に広く使ってほしいので、他の言語で広く使われているHTML・XML処理ライブラリー(Pythonではlxml、RubyではNokogiriというライブラリーがあります。)を参考に、徐々に機能を拡張していく予定です。
ライブラリーは、LuaRocksで公開しており、以下のコマンドで簡単にインストールできます。
例えば、Debian GNU/Linuxでは以下のようにインストールします。
% sudo apt install -y -V libxml2
% sudo luarocks install xmlua
Debian GNU/Linux以外のOSでのインストールは、XMLua - インストールを参照してください。 XMLuaは、Debian GNU/Linuxの他に、Ubuntu、CentOS、macOSに対応しています。
また、XMLuaはLuaJITが提供するFFIライブラリーを使って、C言語の関数やデータ構造にアクセスしているため、XMLuaを使うには、LuaJITが必要になります。お使いのOSのパッケージ管理システムを使って、予めLuaJITもインストールしておいてください。
XMLuaの主な機能を紹介します。 XMLuaを使うとLuaで以下のようなことができます。
XMLuaを使ってHTML、XMLを操作するには、まず、xmlua.Documentオブジェクトを作る必要があります。 xmlua.Documentオブジェクトは、以下のように処理対象のHTMLまたは、XMLをパースして、取得します。
-- "xmlua"モジュールの読み込み
local xmlua = require("xmlua")
local html = [[
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>World</p>
</body>
</html>
]]
-- HTMLをパース
local document = xmlua.HTML.parse(html)
パースする対象は、Luaの文字列型に格納されている必要がありますので、ファイルに保存されているHTMLやXMLをパースする場合は、以下のように事前にファイルから読み込んでからパースする必要があります。
-- "xmlua"モジュールの読み込み
local xmlua = require("xmlua")
local html_file = io.open("test.html")
local html = html_file:read("*all")
html_file:close()
local document = xmlua.HTML.parse(html)
また、以下のようにして、処理したHTML、XMLをxmlua.Documentから元のLuaの文字列に変換することもできます。
-- "xmlua"モジュールの読み込み
local xmlua = require("xmlua")
local html = [[
<html>
<head>
<title>Hello</title>
</head>
<body>
<p>World</p>
</body>
</html>
]]
-- HTMLをパース
local document = xmlua.HTML.parse(html)
-- HTMLへシリアライズ
print(document:to_html())
-- <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
-- <html>
-- <head>
-- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-- <title>Hello</title>
-- </head>
-- <body>
-- <p>World</p>
-- </body>
-- </html>
XMLuaは以下のようにXPathを用いて、要素を検索できます。
local xmlua = require("xmlua")
local xml = [[
<root>
<sub>text1</sub>
<sub>text2</sub>
<sub>text3</sub>
</root>
]]
local document = xmlua.XML.parse(xml)
-- <root>要素配下の全ての<sub>要素を検索します
local all_subs = document:search("/root/sub")
-- "#"を使ってマッチしたノードの数を出力できます。
print(#all_subs) -- -> 3
-- "[]"を使って、N番目のノードにアクセスできます。
print(all_subs[1]:to_xml()) -- -> <sub1>text1</sub1>
print(all_subs[2]:to_xml()) -- -> <sub2>text2</sub2>
print(all_subs[3]:to_xml()) -- -> <sub3>text3</sub3>
上記の検索機能を使うことで、スクレイピングしたWebサイトから特定の要素のみを抜き出すことや、特定の要素が含まれるページを検索する等に利用できます。また、以下のように検索した結果に対して、さらに検索できます。
local xmlua = require("xmlua")
local xml = [[
<root>
<sub class="A"><subsub1/></sub>
<sub class="B"><subsub2/></sub>
<sub class="A"><subsub3/></sub>
</root>
]]
local document = xmlua.XML.parse(xml)
-- 全ての<sub class="A">要素を検索
local class_a_subs = document:search("//sub[@class='A']")
-- <sub class="A">配下の全ての要素を検索
local subsubs_in_class_a = class_a_subs:search("*")
print(#subsubs_in_class_a) -- -> 2
-- /root/sub[@class="A"]/subsub1
print(subsubs_in_class_a[1]:to_xml())
-- <subsub1/>
-- /root/sub[@class="A"]/subsub3
print(subsubs_in_class_a[2]:to_xml())
-- <subsub3/>
XMLuaには、属性値を取得する機能もあり、スクレイピングしたWebページから特定の要素の属性値を抜き出してまとめて処理するといったこともできます。 属性値の取得は以下のように行います。
local xmlua = require("xmlua")
local document = xmlua.XML.parse("<root class='A'/>")
local root = document:root()
-- ドットを使った属性値の取得
print(root.class)
-- -> A
-- []を使った属性値の取得
print(root["class"])
-- -> A
-- get_attributeメソッドを使った属性値の取得
print(root:get_attribute("class"))
-- -> A
XMLuaはマルチスレッドに対応しているため、複数のスレッドから呼び出すことができ、大量のHTMLやXMLを処理する際に効率的に処理することが出来るようになっています。 マルチスレッドで使用するには、幾つかの決まりごとがあるので、マルチスレッドで使用する場合は、XMLua - チュートリアルのマルチスレッドセッションを参照してください。
XMLuaの主な機能を紹介しました。大量にXML・HTMLを操作する必要がある場合の選択肢として、XMLuaを是非使ってみて下さい。
XMLuaの機能についてより詳しく知りたい場合は、XMLua - リファレンスを参照してください。XMLuaの全ての機能の詳細を記載しています。
また、XMLuaをすぐに動かしてみたいという方は、XMLua - チュートリアルを参照してください。ここで紹介した主な機能をすぐに使えるようになるチュートリアルがあります。
開発した成果をフリーソフトウェアで公開するというのは、自社サービスを提供をしてる会社によく見られる光景ですが、クリアコードは自社サービスを持っておらず、このライブラリーは、株式会社セナネットワークス様からの依頼を受けて開発したライブラリーです。つまり受託開発の成果物です。 受託開発の成果物であっても、フリーソフトウェアとして公開することで様々なユーザーがライブラリーを使えます。様々な環境下で使われることにより、いままで発見出来なかったバグを発見出来たり、当初想定されていなかったニーズに気がつけたりして、ライブラリーの品質が高まります。これはお客さんにとってもメリットとなります。
この度、ご依頼主の株式会社セナネットワークス様に上記のようなメリットにご理解をいただき、成果を公開できました。ありがとうございます!
Firefox 57以降のバージョンで採用されているアイコンや配色などの視覚的デザインセットには「Photon」という名前が付いています。サイドバーやツールバーボタンのパネルなど、Firefox用のアドオンで何らかのGUIを提供する場合には、このPhotonと親和性の高い視覚的デザインにしておく事が望ましいです。
Photonのデザイン指針に則って作成されたFirefoxの各種アイコンは、SVG形式のデータが公開されています。SVGのようなベクター画像形式は拡大縮小しても画質が劣化しないため、極端に解像度が高い環境でもレイアウトの崩れや強制的な拡大縮小によるボケなどの発生を気にせずに使えるのが魅力です。PhotonのアイコンセットはライセンスとしてMPL2.0が設定されているため、自作のアドオンにも比較的容易に組み込んで使えますので、是非活用していきたい所です。
アドオンでSVGアイコンを使う時は、背景画像として使う方法が一番簡単です。ただし単純にある要素の背景画像に設定するのではなく、::before
または::after
疑似要素の背景画像として設定する方が、親要素の背景画像や枠線などと組み合わせられて何かと都合が良いのでお薦めです。
例えば何かのGUI要素に「閉じる」ボタンのようなUIを付けたい場合、そのGUI要素にあたる要素が<span href="#" class="closebox"></span>
として定義されているのであれば、タブのクローズボックス等で使われているアイコン画像のclose-16.svg
を以下の要領でアイコン画像として表示できます。
/* 対象の要素そのものではなく、::beforeや::after疑似要素にスタイル指定を行うことで、
アイコン画像の<img>要素を挿入したように扱うことができる。 */
.closebox::after {
/* 疑似要素を有効化するために、空文字を内容として指定する。
(contentが無指定だと::before/::after疑似要素は表示されない。) */
content: "";
/* <img>と同様に、幅と高さを持つボックス状のインライン要素として取り扱う。 */
display: inline-block;
/* 表示したいアイコン画像の大きさを、この疑似要素自体の大きさとして設定する。 */
min-height: 24px;
min-width: 24px;
/* SVG画像をボックスの大きさぴったりに拡大縮小して背景画像として表示する。背景色は透明にする。 */
background: transparent url("./close-16.svg") no-repeat center / 100%;
}
Photonのアイコン画像はいずれも黒または白一色のべた塗りで、意味はシルエット(形)で表すようにデザインされています。これには、カラフルなアイコンだとテーマの色に合わなくなる事があるのに対し、シルエットのみのアイコンであればテーマに合わせた色に変えて使いやすいからという理由があります。
実は、背景画像として表示されるSVG画像の色は、FirefoxにおいてはCSSでの指定だけで変える事ができます。SVG画像の中で<path fill="context-fill">
のようにfill="context-fill"
が設定されている閉領域の塗り潰し色は、以下のようにするとCSSの側での指定を反映させられます。
.closebox::after {
content: "";
display: inline-block;
min-height: 24px;
min-width: 24px;
background: transparent url("./close-16.svg") no-repeat center / 100%;
/* 塗りの色を指定 */
fill: #EFEFEE;
/* CSSのfillプロパティの値をSVG画像のcontext-fillに反映するための指定 */
-moz-context-properties: fill;
}
ここではカラーコードを直接指定していますが、以下のようにカスタムプロパティ(CSS変数)を使えば色の指定だけを簡単に差し替えられます。
:root {
/* 冒頭、最上位の要素で色だけを定義 */
--background-base-color: #EFEFFF;
--foreground-base-color: #0D0D0C
}
...
.closebox::after {
...
/* 後の箇所では定義済みの色を名前で参照する */
fill: var(--foreground-base-color);
...
}
これをうまく使えば、ツリー型タブのように複数テーマを切り替えたりthemes.onUpdated
を監視して他のアドオンが設定したテーマの色を自動的に反映したりといった事も容易に実現できます。
ただし、ここで1つ残念なお知らせがあります。実は、上記の指定はFirefoxの既定の状態では機能しないのです(Firefox 57現在)。
上記のような指定はFirefox自体のGUIの外観を定義するのにも使われているのですが、ここでfill
と共に使われている-moz-context-properties
が曲者です。このプロパティは今のところFirefoxの独自拡張プロパティで、about:config
でproperties.content.enabled
をtrue
に変更しない限り、アドオンが提供するサイドバーやツールバーのポップアップなどの中では使えないようになっているため、結局はSVGのアイコン画像は黒一色で表示されるという結果になってしまうのです。Bug 1388193またはBug 1421329が解消されるまでは、この方法は一般的なユーザーの環境では使えないという事になります。
でも諦めるのはまだ早いです。Photonのアイコンセットのようにシルエットだけで構成されたSVG画像であれば、mask
関連の機能で上記の例と同等の事ができます。具体的には以下の要領です。
.closebox::after {
content: "";
display: inline-block;
min-height: 24px;
min-width: 24px;
/* SVG画像は背景画像としては使わない。 */
/* background: transparent url("./close-16.svg") no-repeat center / 100%; */
/* まず、アイコンの色として使いたい色で背景を塗り潰す */
background: #EFEFEE;
/* 次に、SVG画像をボックスの大きさぴったりに拡大縮小してマスク画像として反映する */
mask: url("./close-16.svg") no-repeat center / 100%;
}
疑似要素自体は指定の背景色の矩形として描画されますが、その際マスク画像の形に切り抜かれるため、結果として「単色で、SVG画像のシルエットの形をしたアイコン」のように表示されるという仕組みです。
この代替手法には元の手法よりもCPU負荷が高くなるというデメリットがあります。特に:hover
等の疑似クラスやアニメーション効果と組み合わせる時には、CPU負荷が一時的に100%に張り付くようになる場合もあり得ます。モバイルPCの電池の持ちが悪くなるなどの副作用が生じる事になりますので、使用は注意深く行ってください。
Bug 1388193またはBug 1421329のどちらかが解消された後は、この代替手法を速やかに削除できるように、最上位の要素のクラスなどを見て反映するスタイル指定を切り替えるのがお薦めです。以下はその指定例です。
.closebox::after {
content: "";
display: inline-block;
min-height: 16px;
min-width: 16px;
/* 将来的に反映したい指定 */
background: url("./close-16.svg") no-repeat center / 100%;
fill: #EFEFFF;
-moz-context-properties: fill;
}
:root.simulate-svg-context-fill .closebox::after {
/* 後方互換性のための代替手法の指定 */
background: #EFEFFF;
mask: url("./close-16.svg") no-repeat center / 100%;
}
FirefoxのアドオンでSVG画像をアイコンとして使う場合の小技をご紹介しました。
GUIを持つアドオンを作る場合、ユーザーを迷わせないで済むように、アイコン画像はなるべくFirefox本体の物とデザインを揃えておいた方が良いです。Photonのアイコンセットを使い、皆さんも洗練されたデザインのGUIを実装しましょう。
fluent-plugin-elasticsearchはよく使われているプラグインではありましたが、長らく開発が滞っていました。 @pitrさんと@dterrorさんからコミット権をもらって gem owner に加えてもらったので 本プラグインの開発を引き取りました。
開発を引き取ってからの主な変更点は以下の通りです。
fluent-plugin-elasticsearchの使い方はREADME.mdを参照してください。
なお、fluent-plugin-elasticsearch 2.0.0以降は後方互換性がなく、Fluentd v0.14以降でのみ動作することに注意してください。 バージョン2.0.0以降ではナノ秒が有効になるため、一秒に複数のレコードが記録されるログ基盤で本プラグインを使用している場合は時刻で並び替えたときに順番が不定になることが軽減されます。
Fluentd v0.14/v1.0ではより柔軟なbufferの設定が可能になっています。そのため、新規で本プラグインを使用する際にはbufferセクションの中にbufferの設定を書くようにしてください。
Fluentd v0.14/v1.0における <buffer>
セクションの書き方については公式のbufferセクションのドキュメントを参照してください。
本プラグインの開発を引き取ったことと、新しく入った機能について軽く紹介しました。
@y-kenさんからコミット権をもらって gem owner に加えてもらったのでfluent-plugin-geoip v1.0.0などをリリースしました。
v1.1.0までの主な変更点は以下の通りです。
GeoIP2の使い方は以前の記事やREADME.mdを参照してください。
なお、ダウンロード可能なGeoIP2のデータベースは複数ありますが、geoip2_cは全てのデータベースを扱うことができるので、fluent-plugin-geoipでもバックエンドをgeoip2_cにしていれば全てのデータベースを扱うことができます。 しかし、fluent-plugin-geoipは1つのイベントに対して複数のデータベースを検索することはサポートしていないので、1つのイベントに対して複数のデータベースから取得した情報を付加したい場合は以下のように複数回フィルターを適用すればよいです。
<filter>
@type geoip
@id city
geoip2_database /path/to/GeoLite2-City.mmdb
<record>
city ${city.names.en["host"]}
</record>
</filter>
<filter>
@type geoip
@id asn
geoip2_database /path/to/GeoLite2-ASN.mmdb
<record>
asn_id ${autonomous_system_number["host"]}
asn_organization ${autonomous_system_organization["host"]}
</record>
</filter>
2017-12-25 12:33:34.019899208 +0900 raw.dummy: {"host":"66.102.9.80","message":"test","city":"Mountain View","asn_id":15169,"asn_organization":"Google LLC"}
2017-12-25 12:33:35.021822991 +0900 raw.dummy: {"host":"66.103.9.81","message":"test","city":"Hollywood","asn_id":23089,"asn_organization":"Hotwire Communications"}
これはgeoip2_cを使うようになってからできることだったのですが、あまり知られていないようなので設定例と一緒に紹介しました。
fluent-plugin-geoip v1.0.0での主な変更点とできるようになったことを紹介しました。