株式会社クリアコード > ククログ

ククログ


Windwos 10のWSLを日常的に活用する:Windowsの補助としてLinuxコマンドを使う

前の記事では、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のコマンド

WSLを有効化済みの環境では、Windowsのコマンドプロンプトでwslconfigwslという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>

WindowsアプリケーションからWSLの機能を呼び出す

前述のwslコマンドの実体はC:\Windows\System32\wsl.exeです。外部アプリケーションを呼び出す機能を持ったアプリケーションからこのファイルを実行するようにすれば、WindowsアプリケーションからWSL経由でLinux環境のコマンドを実行する事ができます。

例えばEmEditorというテキストエディタは、無料版での使用においても、「ツール」→「外部ツール」→「外部ツールの設定」で任意のアプリケーションを登録できます。以下は、これを使って選択範囲の文字列をBashのワンライナーで加工する例です。

  • 例1: 選択範囲の文字数を数えてツールチップで表示する
    • タイトル:選択範囲の文字数を数える
    • コマンド:C:\Windows\System32\wsl.exe
    • 引数:bash -c "nkf -w | wc -c"
    • アウトプットバーを使用する:ON
    • 終了時に閉じる:ON
    • 入力:選択テキスト
    • 出力:ツールチップとして表示
    • 出力→エンコード:システム既定(932, shift_jis)
  • 例2: 選択範囲の行数を数えてツールチップで表示する
    • タイトル:選択範囲の行数を数える
    • コマンド:C:\Windows\System32\wsl.exe
    • 引数:bash -c "nkf -w | wc -l"
    • アウトプットバーを使用する:ON
    • 終了時に閉じる:ON
    • 入力:選択テキスト
    • 出力:ツールチップとして表示
    • 出力→エンコード:システム既定(932, shift_jis)
  • 例3: 選択範囲の行をアルファベット順・数値順に並べ替える
    • タイトル:選択範囲の行を並べ替える
    • コマンド:C:\Windows\System32\wsl.exe
    • 引数:bash -c "nkf -w | sort -n | nkf -s"
    • アウトプットバーを使用する:ON
    • 終了時に閉じる:ON
    • 入力:選択テキスト
    • 出力:選択範囲と置換
    • 出力→エンコード:システム既定(932, shift_jis)

「実行ファイルとしてwsl.exeを指定してbash -c "〜"でコマンド列を渡す」という点と、「標準入出力をnkfなどを使って変換して呼び出し元のWindowsアプリに合わせる」という点が重要です。これらの点に気をつければ、gemnpmでインストールされたコマンドなども容易に組み合わせられるため、応用の幅は無限大と言えます。また、ワンライナーで記述するのが大変な処理は、あらかじめWSLの環境にシェルスクリプトを用意しておきそれをC:\Windows\System32\wsl.exe bash "/home/user/script.sh"のように実行するようにしてもよいでしょう。

まとめ

以上、WSLをWindowsアプリケーションの機能強化に使う手順を、EmEditorでの設定を例としてご紹介しました。

gemnpmでインストールできるコマンドの中には、特に明記はされていなくても、LinuxやmacOSの環境のみを想定して作られているという物が度々あります。特に拡張モジュール(バイナリ)を含む物はその傾向が顕著で、Windows上で動作させるにはVisualStudioの導入が必要であるなど、気軽に使えるとは言い難いのが実情です。

しかしWSL上のLinux環境であれば、そのようなコマンドも難なくインストールできます。「Windows上でgemnpmを使う」場面に特有の苦労をしなくてもよくなりますので、使用にあたってのハードルが大きく下がりますので、今まで「便利そうなコマンドだけど、Windowsじゃ動かないんだよな……」と諦めていた人は、是非一度試してみて下さい。

2017-12-04

PGConf.ASIA 2017 - PGroonga 2 – Make PostgreSQL rich full text search system backend! #pgconfasia

PGConf.ASIA 2017RUMの存在を知った須藤です。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関連でなにか相談したいことがある場合はお問い合わせください。

タグ: Groonga
2017-12-07

Firefox Quantum以降のglobalChrome.cssの移行について

はじめに

企業利用においては、Firefox/Thunderbirdの特定の機能を使わせないようにカスタマイズしたいという場合があります。 そのための機能を提供するアドオンにglobalChrome.cssがあります。 (使用例は過去のThunderbirdのインターフェースをカスタマイズするにはという記事をご参照下さい。)

しかし、WebExtensions APIへの移行の流れにともない、Firefox ESR59以降ではglobalChrome.cssは使えないことが決まっています。また、Firefox Quantum以降ではすでにアドオンの互換性がなくなっています。

今回は、globalChrome.cssで行っていたカスタマイズを別の手段で代替する方法と、その注意点をご紹介します。

globalChrome.cssとは

Firefox/Thunderbirdは、アプリケーション自体のUIがXMLとCSSで構成されています。 このアドオンは、Firefox/ThunderbirdのUIに追加のスタイルシートを反映することで、その外観を変化させるというアドオンです。 冒頭で紹介したように、企業利用においては不要な機能をあらかじめ削除するなど、ユーザーインターフェースをその企業に合わせてカスタマイズした状態で利用したい場合に使われます。

しかしながら、前述の通りこのアドオンはFirefox Quantum以降では動作しません。 これを使ってFirefox/Thunderbirdをカスタマイズしている場合、代替手段が必要です。 それがユーザースタイルシートと呼ばれるものです。*1

ユーザースタイルシートには次の2つがあります。

  • userChrome.css
  • userContent.css

globalChrome.cssはこのうちuserChrome.cssにて代替することができます。

userChrome.cssとuserContent.cssの違い

ユーザースタイルシートといわれるのがこれら2つのファイルですが、違いはなんでしょうか。 Mozilla のカスタマイズには、次のような説明があります。

  • userChrome.cssは「Mozilla アプリケーションの UI クロームに関するCSSを管理」
  • userContent.cssは「ウィンドウ内のコンテンツに関するCSSを管理」

これらのファイルの違いをuserChrome.cssとuserContent.cssのサンプル設定から示します。

userChrome.cssには文字色を赤にする全称セレクタの設定を適用してみます。

*|* {
    color: red !important;
}

userContent.cssには文字色を青にする全称セレクタの設定を適用してみます。

*|* {
    color: blue !important;
}

すると、UIクローム部分は赤い文字に、コンテンツ領域は青い文字になります。 この事から、それぞれのユーザースタイルシートがFirefox/Thunderbirdのどの部分に作用するかが分かります。

どの部分に作用するかを示すスクリーンショット

globalChrome.cssからuserChrome.cssに移行するポイント

globalChrome.cssuserChrome.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の導入やカスタマイズでお困りで、自力での解決が難しいという場合には、有償サポート窓口までぜひ一度ご相談下さい。

*1 アドオンの互換性に問題がでるため用意された新たな仕組みではなく、ユーザースタイルシート自体は昔からあるものです。

*2 プロファイルフォルダに特定のファイルをコピーする必要があった事例に関して、autoconfig.cfgにて対応したケースが過去にあります。

2017-12-08

OSS Gate東京ふりかえり2017-12を開催 #oss_gate

あっという間に一週間が経ってしまう須藤です。

一週間ほど前の2017年12月05日にドリコムさんOSS Gate東京ふりかえり2017-12を開催しました。

これはよりうまくOSS Gateに取り組むための活動です。最近の4ヶ月で得られた知見を活用して、次の4ヶ月はもっとうまく活動します。去年は1年に1回だけ実施しましたが、それだとフィードバックの間隔が開きすぎて得られた知見を活かしきれないという知見が得られました。そのため、今年は4ヶ月に1回の開催にチャレンジしています。(去年よりも今年の方がうまく活動できるようにしている。)

今回は今年最後の開催でした。これまでは、事前にオンラインで知見を集めて、東京でオフラインで集まる、というやり方でしたが、今回は東京と大阪をビデオ会議システムでつないで開催しました。(今回のチャレンジ。)東京と大阪ではOSS Gateへの取り組み方が少し異なるので、違うやり方で得られた知見をお互いに共有して次に活かせることを期待していました。

実際にやってみた結果ですが、東京と大阪をつないでよかったです。今までにはない発想を得られました。1つ例を紹介します。

東京ではワークショップのビギナー(OSSの開発に参加したいけど未経験の人)とサポーター(ビギナーをサポートする人)の割合のバランスが悪いという課題があります。ビギナーは口コミで毎回たくさん新しい人が集まるけど、サポーターはなかなか新しい人が増えていません。そのため、サポーターが不足しがちです。

これに対し、東京ではサポーター1人でいかに多くのビギナーをサポートするかという方向でがんばっていました。一方、大阪ではビギナーとサポーターが1: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にサポーターとして参加しにきてね!!!

2017-12-13

Firefoxのクラッシュレポートを解析するには

はじめに

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のクラッシュレポートを解析する方法について紹介しました。 実際にクラッシュレポートの内容をどう活用するかについては、別の記事にて紹介します。

2017-12-14

Firefoxを意図的にクラッシュさせる方法(2017年版)

はじめに

Firefoxの導入時の要件として、クラッシュ時のレポートを送信しないようにするという設定を行う事があります。 この設定が意図通りに反映されているかどうかを確認するために、Firefoxが実際にクラッシュした時の様子を観察したい場合があります。

Firefoxには既知で且つ未修正のクラッシュバグがいくつかあるため、それを突くようなコードを実行すればFirefoxをクラッシュさせる事ができます。しかし、クラッシュバグは再現性が低い物もありますし、そもそも新しいバージョンのFirefoxではクラッシュしないように修正されていることも多いです。安定してFirefoxをクラッシュさせる方法を把握しておけば、Firefoxをクラッシュさせるための方法をその都度あれこれ調べなくても済みます。

以前、Firefoxを意図的にクラッシュさせる方法として js-ctypes を利用してFirefoxをクラッシュさせる方法を紹介しました。今回はその記事の更新版としてより簡単な方法を紹介します。

crashIfNotInAutomationを使った方法

Firefox 55以降では、意図的にクラッシュさせる方法が用意されています。それが Components.utils.crashIfNotInAutomation です。

実際にこれを使ってクラッシュさせるには次の手順を踏みます。

  1. Firefoxを起動する。
  2. about:config を開き、devtools.chrome.enabled の値を true に設定する。
  3. Ctrl-Shift-Jを押すか、「ウェブ開発」メニューから「ブラウザコンソール」を選択してブラウザコンソールをを起動する。
  4. プロンプトに「Components.utils.crashIfNotInAutomation()」を入力してEnterを押下する。

ブラウザコンソール画像

これでFirefoxを実際にクラッシュさせることができます。

まとめ

今回は、Firefoxを意図的にクラッシュさせる最新の方法を紹介しました。 Firefox 55以降で導入された仕組みのため、それ以前のFirefoxではFirefoxを意図的にクラッシュさせる方法で紹介した方法がよいでしょう。*1

*1 js-ctypesを使った方法はFirefox 57でも依然として有効です。

2017-12-15

Debian SourcesのAPIを使ってパッチが数多く適用されているパッケージを調べるには

はじめに

今年も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

今後そういった情報も含まれるともっと活用の幅が広がるかもしれません。

*1 DEP3の情報が含まれていたら、まだアップストリームに報告していないパッチをたくさん抱えているパッケージをリストアップすることができた。

*2 sources.debian.org の各パッケージの debian/patches フォルダを確認するのが手っ取り早そうです。

2017-12-18

Firefoxのクラッシュレポートの解析結果の読み方

はじめに

Firefoxの法人向けサポートにおいては、クラッシュした際のクラッシュレポートファイルを提供してもらって、問題の切り分けをするということがあります。

以前、そのような場合に有用なFirefoxのクラッシュレポートを解析する方法をFirefoxのクラッシュレポートを解析するにはという記事で紹介しました。

今回は、実際にクラッシュレポートの解析結果をどのように読み解いたらよいのかを説明します。

Mozilla Crash Reportsでわかること

前回の記事ではクラッシュレポートを送信してMozilla Crash Reportsにて確認するところまでを紹介しました。

送信したクラッシュレポートの解析結果は次のようにカテゴリに分けてタブで表示されます。

カテゴリの内容

カテゴリ 説明
Details クラッシュレポートのスタックトレースを表示
Metadata 端末の搭載メモリなどの情報を表示
Bugzilla スタックトレースから自動検出された関連するバグのリストを表示
Modules 名前解決に必要なシンボル(.pdb)ファイルのリストを表示
Raw Dump クラッシュした際のダンプデータをテキスト形式で表示
Extensions クラッシュした際にインストールされていたアドオンのリストを表示
Telemetry Environment アドオンやプラグインの設定情報を表示

着目するポイントは DetailsBugzilla です。

Details にはクラッシュレポートのスタックトレースが表示されます。

Details の内容

上記の例だと、 Frame 0 にて xul.dllnsINode::GetAsText() でクラッシュしたことがわかります。 また、Source カラムからクラッシュした該当行のソースコードも知ることができます。

次に着目するのは、 Bugzilla です。スタックトレースから自動的に検出された関連しそうなバグを表示してくれます。

Bugzilla の内容

場合によっては、ここで提示されたバグの内容と実際にクラッシュした原因が一致し、問題の特定につながることがあります。

クラッシュした内容が既知のバグであれば、どのような状況でクラッシュするのかの補足情報が得られます。 すでに修正済みであればどのバージョンでその修正がリリースされるのか、あるいはバックポートされないのかという情報も得られます。

まとめ

今回は、実際にクラッシュレポートの解析結果をどのように読み解いたらよいのかを説明しました。

DetailsBugzilla の内容を確認するだけでも問題解決には有用です。

FirefoxやThunderbirdの導入やカスタマイズ、クラッシュなどの現象でお困りで、自力での解決が難しいという場合には、有償サポート窓口までぜひ一度ご相談下さい。

2017-12-19

Firefoxのアドオンで組み込みのページを提供する場合の注意点

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.jsoncontent_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箇所が該当しました。

  1. コンテントスクリプトの名前空間の初期化に関わっていそうな箇所
  2. アドオンに含まれるページの名前空間の初期化に関わっていそうな箇所
  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を参照して各ユーザーのアドオンのインストール状況を調べユーザーの動向をトラッキングする「フィンガープリンティング」を防止するための仕様です。

タグ: Mozilla
2017-12-20

Firefoxのアドオンで適切な終了処理を実装する方法

ソフトウェアをアンインストールする際には、ゴミや痕跡を無駄に残さない事が望ましいです。また、イベントを監視する必要のある機能を含んでいる場合、監視の必要がなくなったにも関わらず監視を続けていると、メモリや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イベントを捕捉するという物です。このような用途に使えそうな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の組み合わせで実現する方法が無いか検討してみて下さい。

タグ: Mozilla
2017-12-21

Firefoxのアドオンで、一般的な方法では分からないタブの状態を判別する

Firefoxのタブを参照するアドオンは、browser.tabs.get()browser.tabs.query()などのAPIを使って各タブの状態を取得します。この時、Firefoxのタブの状態を表すオブジェクトはtabs.Tabという形式のオブジェクトで返されます。

tabs.Tabにはタブの状態を表すプロパティが多数存在していますが、ここに表れないタブの状態という物もあります。「未読」「複製された」「復元された」といった状態はその代表例です。これらはWebExtensions APIの通常の使い方では分からないタブの状態なのですが、若干の工夫で判別することができます。

タブが未読かどうかを判別する方法

タブの未読状態は、バックグラウンドのタブの中でページが再読み込みされたりページのタイトルが変化したりしたらそのタブは「未読」となり、タブがフォーカスされると「既読」となります。これは、tabs.onUpdatedtitleの変化を監視しつつ、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()で設定された情報を引き継ぎます。この性質を使うと、以下の理屈でタブの種類を判別できます。

  1. browser.sessions.getTabValue()でIDを取得してみて、取得に失敗したら(IDが保存されていなければ)そのタブは新しく開かれたタブである。
  2. IDの取得に成功し、そのIDを持つタブが既に他に存在しているのであれば、そのタブは複製されたタブである。
  3. IDの取得に成功し、そのIDを持つタブが他に存在していないのであれば、そのタブは一旦閉じられた後に復元されたタブである。

(この判別方法にはFirefox 57以降で実装されたsessions APIbrowser.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.onUpdatedtabs.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が無いからと諦めていた事について実現の余地が見つかるかもしれませんので、色々試してみる事をお薦めします

タグ: Mozilla
2017-12-22

Lua用HTML・XML処理ライブラリー XMLua

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の主な機能を紹介します。 XMLuaを使うとLuaで以下のようなことができます。

HTML、XMLドキュメントのパース/シリアライズ

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>
XPathを使った、要素の検索と属性値の取得

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 - チュートリアルを参照してください。ここで紹介した主な機能をすぐに使えるようになるチュートリアルがあります。

開発した成果をフリーソフトウェアで公開するというのは、自社サービスを提供をしてる会社によく見られる光景ですが、クリアコードは自社サービスを持っておらず、このライブラリーは、株式会社セナネットワークス様からの依頼を受けて開発したライブラリーです。つまり受託開発の成果物です。 受託開発の成果物であっても、フリーソフトウェアとして公開することで様々なユーザーがライブラリーを使えます。様々な環境下で使われることにより、いままで発見出来なかったバグを発見出来たり、当初想定されていなかったニーズに気がつけたりして、ライブラリーの品質が高まります。これはお客さんにとってもメリットとなります。

この度、ご依頼主の株式会社セナネットワークス様に上記のようなメリットにご理解をいただき、成果を公開できました。ありがとうございます!

2017-12-25

FirefoxのアドオンでSVG画像を「色を変えられるアイコン」として使う方法

Photonのアイコン画像を劣化の無いSVG形式で入手する

Firefox 57以降のバージョンで採用されているアイコンや配色などの視覚的デザインセットには「Photon」という名前が付いています。サイドバーやツールバーボタンのパネルなど、Firefox用のアドオンで何らかのGUIを提供する場合には、このPhotonと親和性の高い視覚的デザインにしておく事が望ましいです。

(Photon Iconsのスクリーンショット) Photonのデザイン指針に則って作成されたFirefoxの各種アイコンは、SVG形式のデータが公開されています。SVGのようなベクター画像形式は拡大縮小しても画質が劣化しないため、極端に解像度が高い環境でもレイアウトの崩れや強制的な拡大縮小によるボケなどの発生を気にせずに使えるのが魅力です。PhotonのアイコンセットはライセンスとしてMPL2.0が設定されているため、自作のアドオンにも比較的容易に組み込んで使えますので、是非活用していきたい所です。

SVGアイコンの簡単な使い方

アドオンでSVGアイコンを使う時は、背景画像として使う方法が一番簡単です。ただし単純にある要素の背景画像に設定するのではなく、::beforeまたは::after疑似要素の背景画像として設定する方が、親要素の背景画像や枠線などと組み合わせられて何かと都合が良いのでお薦めです。

例えば何かのGUI要素に「閉じる」ボタンのようなUIを付けたい場合、そのGUI要素にあたる要素が<span href="#" class="closebox"></span>として定義されているのであれば、タブのクローズボックス等で使われているアイコン画像のclose-16.svgを以下の要領でアイコン画像として表示できます。 (::after疑似要素として表示された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%;
}

SVGアイコンの色を変えるには?

Photonのアイコン画像はいずれも黒または白一色のべた塗りで、意味はシルエット(形)で表すようにデザインされています。これには、カラフルなアイコンだとテーマの色に合わなくなる事があるのに対し、シルエットのみのアイコンであればテーマに合わせた色に変えて使いやすいからという理由があります。

実は、背景画像として表示されるSVG画像の色は、FirefoxにおいてはCSSでの指定だけで変える事ができます。SVG画像の中で<path fill="context-fill">のようにfill="context-fill"が設定されている閉領域の塗り潰し色は、以下のようにするとCSSの側での指定を反映させられます。 (同じSVG画像を使用して、塗り潰しの色を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:configproperties.content.enabledtrueに変更しない限り、アドオンが提供するサイドバーやツールバーのポップアップなどの中では使えないようになっているため、結局はSVGのアイコン画像は黒一色で表示されるという結果になってしまうのです。Bug 1388193またはBug 1421329が解消されるまでは、この方法は一般的なユーザーの環境では使えないという事になります。

maskを使った「CSSの指定だけでSVGアイコンの色を変える」代替手法

でも諦めるのはまだ早いです。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を実装しましょう。

タグ: Mozilla
2017-12-26

fluent-plugin-elasticsearch の開発を引き取りました

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セクションのドキュメントを参照してください。

まとめ

本プラグインの開発を引き取ったことと、新しく入った機能について軽く紹介しました。

タグ: Fluentd
2017-12-27

Fluent-plugin-geoipのメンテナになりました

@y-kenさんからコミット権をもらって gem owner に加えてもらったのでfluent-plugin-geoip v1.0.0などをリリースしました。

v1.1.0までの主な変更点は以下の通りです。

  • Fluentd v1のプラグインAPIに移行した
    • Fluentd v0.12.x については fluent-plugin-geoip 0.8.x でサポートする
  • geoip2_cがデフォルトのバックエンドとなった
  • ドキュメントを更新した
    • 主にfilter_geoipを使ってもらうために、filter_geoipの説明を前に移した
    • 設定の説明を追加した
  • latitude, longitude にはデフォルト値を設定するようにした
  • filter_geoipでtagやtimeなどをレコードに挿入できるようにした
    • out_geoipではサポートしていた機能
  • どんなキーを指定すればよいのか簡単に調べるためにgeoip,geoip2のデータを確認するためのスクリプトを追加した

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での主な変更点とできるようになったことを紹介しました。

タグ: Fluentd
2017-12-28

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|