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

ククログ

タグ:

ある日突然Thunderbirdのアドオンが動作しなくなった、という時の原因の切り分け方

FirefoxやThunderbirdのアドオンを使用していて、ある日急に動かなくなった、という話はよくあります。 そのような場合にどうやって原因を特定し解決するのか、Thunderbird用のアドオンであるTheme Font & Size Changer for TBでの場合を例に解説してみます。 トラブルシューティングの一時例として参考にしていただければ幸いです。

アドオンの性質を見極める

まず何を置いても最初に確認するべきは、そのアドオンがどのような性質を持ちどのような機能を実現するアドオンなのかという点です。

今回が起こった「Theme Font & Size Changer for TB」はThunderbirdのGUIの文字サイズや配色を変更するアドオンです。 Thunderbirdではメールの内容を解釈したり表示したりする部分と全体的なUIを制御する部分とはほとんど関係がないため、例えば「受信したメールの内容」は問題と無関係である可能性が高い、といった推測ができます。 また、特定のWebサービスと連携するようなアドオンでもないため、Webサービス側からの接続遮断や、Webサービスのメンテナンスによる機能停止といった物もこの問題には関係していないと考えられます。

このように、原因究明を迅速に行うためには、可能性が低い事柄を調査の対象から一旦除外するのが有効です。

問題が起こるようになる直前に自分で行った操作の影響を疑う

次に確認するべきなのは、可能性として除外しきれなかった範囲における、問題が発生し始めた時期に行った操作との関連性です。 例えば以下のような事柄です。

  • Thunderbirdの設定を変更した
  • アドオンを新しくインストールした
  • アドオンをアンインストールした

変化が可逆的な物である場合、まずそれを元に戻してみるのが基本です。 今回の事例では問題の発生前後で特にこれといった操作を行っておらず(休暇明けに出勤したら突然アドオンが動作しなくなっていた、のような状況を想像して下さい)、ユーザーが行った操作は原因ではない可能性が高いと思われました。

Thunderbird自体の更新の影響を疑う

Thunderbirdは自動更新機能を持っており、ユーザーが気がつかないうちに新しいバージョンがダウンロードされている事があります。 アドオンが新しいバージョンのThunderbirdに対応していない場合、Thunderbirdの自動更新に伴ってアドオンが無効化されたり、アドオンが動作しなくなったりする事があります。

今回の事例では、問題の発生前後でThunderbirdの新しいリリースは行われていませんでした。 ですので、この可能性は除外しました。

アドオンの更新の影響を疑う

Thunderbird本体と同様に、自動更新によってアドオンが新しいバージョンに入れ替わった事で問題が起こる事もあります。 一般的にはあまりありませんが、「更新前は正常に動いていた機能が更新後には動かなくなる」という種類の問題(後退バグ、あるいはリグレッション)が混入する場合があるからです。

また、問題が起こっているアドオンそのもの以外の他のアドオンが更新された事がきっかけで問題が起こる事もあります。 アドオン同士が衝突して片方あるいは両方の動作が妨げられるという事例は枚挙にいとまがありません。

ただ、今回の事例では、問題の発生前後でアドオンの更新は特に発生していませんでした。 問題が起こっているアドオンの最終更新日は2017年11月で、だいぶ以前の事ですので、自動更新が急に行われたという事は考えにくいです。

アドオン自体の問題を疑う

問題の発生前後で特にこれといった変化が無かったとなると、後はアドオンの初期化処理を丁寧に追って、どの時点で問題が発生しているのかを突き止めるという事になります。

Thunderbird用アドオンは、現在Firefox用のアドオンで「レガシー」と位置づけられている種類のアドオンと同様の作りになっています。 今回問題となったアドオンの、問題発生時の最新版であるバージョン62.0は、その中でもBootstrapped Extensionと呼ばれる、アドオンのインストール・アンインストール時にThunderbird自体の再起動が不要な種類の物でした。

Bootstrapped Extensionの初期化処理のステップを追うと、以下の箇所のThemeFontSizeChangerBootstrapAddon.compile()falseを返しているために初期化処理が中断されているという事が分かりました。

function startup(data, reason) {
    try{
        if(!ThemeFontSizeChangerBootstrapAddon.compile()) return;
        ThemeFontSizeChangerBootstrapAddon.startup(data,reason);
    } catch(e){
        ThemeFontSizeChangerBootstrapAddon.lg(e,1);
    }
}

さらにこのメソッドの内容を調査すると、以下の通り実装されている事が分かりました。

...
const build = 1507391194;
...
    compile:function(){
        if((build+'').length != 10) Services.prompt.alert(null, 'CompileAddon', 'An error occured');
        if(new Date().getTime() > ((build+'').length == 10 ? build*1000 : build)+(3*30*24*60*60*1000)) return false;
        return true;
    }

new Date().getTime()は実行時のUNIX時刻をミリ秒単位で取得する物です。1507391194というのはいわゆるUNIX時間(協定世界時1970年1月1日0時0分0秒からの経過秒数による日時の表現)で2017年10月7日15時46分34秒を指しており、この箇所全体では「現在日時がその3ヶ月後(2018年1月5日15時46分34秒)を超えているとfalseが返される」という意味になっています。 つまり、それまでは全く正常に動作していたこのアドオンは、世界中で今年の1月5日以降急に動作しなくなるよう設計されていたという、時限式のタイマーが組み込まれていたという事が分かりました。

以上の事から、この問題の対策として「当該箇所を無害化する改修を施す」あるいは「実行環境の時計を意図的にずらす」のいずれかを実施する必要があるという事になります。

まとめ

「Theme Font & Size Changer for TB」を例にとって、アドオンが急に動作しなくなったという場合の調査の具体的な進め方をご紹介しました。

当社のFirefox/Thunderbrid法人向けサポートでは、アドオンに関するものについても、障害の原因調査から対策のご提案、改修版のアドオンの提供や開発元へのフィードバックなどの対応を有償にて行っております。 業務で使用しているアドオンで致命的な問題が発生していてお困りの場合には、ぜひ一度当社までご相談下さい

タグ: Mozilla
2018-01-16

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

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

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のアドオンで組み込みのページを提供する場合の注意点

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

Thunderbirdの設定値の変更を監視するには

はじめに

Thunderbirdのアドオンを作成する際に、設定値の変化を検出したい場合があります。 具体的には、次のようなケースです:

  • ユーザーが関連する設定値を変更した時に、その効果を即座に反映させたい
  • アカウントの登録や変更のタイミングで、特定の処理をフックさせたい

MozillaのXPCOMライブラリには、いわゆる「オブザーバー」の仕組みが備わっています。 この仕組みを利用すれば、上記のような処理を比較的手軽に実装することができます。

以下の記事では、Thunderbirdの設定値の変更を検知して、任意の処理をフックする方法を解説いたします。

具体的な実装方法

nsIPrefBranch インターフェイスに定義されている addObserver() メソッドを利用します。 例として、Thunderbirdの自動更新フラグ app.update.auto を監視するコードのサンプルを以下に示します:

// ブランチオブジェクトを取得する
var Cc = Components.classes;
var Ci = Components.interfaces;
var prefs = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefBranch);

// 設定値にオブザーバーを登録する
prefs.addObserver('app.update.auto', function(aSubject, aTopic, aData) {
    // 設定が変更された時の処理
}, false);

このように定義すると、設定値 app.update.auto が変更されるたびに、二番目の引数で与えたオブザーバー関数が呼び出されるようになります(オブザーバー関数の引数については次節で説明します)。なお、最後の引数は「オブザーバーを弱参照で保持するか否か」を制御するブール値です。今回の例では単純にfalse(=通常の参照を持つ)を指定しています *1

コールバック関数の引数について

登録したオブザーバー関数は、次の三つの引数を伴って呼び出されます:

引数名 内容
aSubject 監視対象のブランチオブジェクト
aTopic 文字列 "nsPref:changed"(固定値)
aData 変更された設定名

このうちaSubjectaDataを組み合わせると、コールバック内で変更後の設定値を取得できます。以下に具体的な利用例を示します:

prefs.addObserver('app.update.auto', function(aSubject, aTopic, aData) {
    aSubject.QueryInterface(Ci.nsIPrefBranch);
    var isAutoUpdate = aSubject.getBoolPref(aData);
    if (isAutoUpdate) {
        // Thunderbirdの自動更新がONの場合
    } else {
        // Thunderbirdの自動更新がOFFの場合
    }
}, false);
複数の設定値をまとめて監視する

Thunderbirdの設定値は、一般に木構造をなしています。

実は addObserver() を使うと、末端の葉ノードだけではなく、中間にある内部ノードに対してオブザーバーを登録することもできます。この場合、対象のノードのすべての子孫ノードの変更について、登録したオブザーバー関数が呼び出されます。

例えば、app.update配下の設定値をまとめて監視したい場合は次のように記述します:

prefs.addObserver('app.update', function(aSubject, aTopic, aData) {
    switch (aData) {
      case 'app.update.auto':
        ...
        break;
      case 'app.update.enabled':
        ...
        break
    }
}, false);

この記法は、自分のアドオンの設定値を一括して管理したい場合などに非常に有効です。

まとめ

本記事では、Thunderbirdの設定値の変更を検知して、任意の処理をフックする方法を解説しました。

この仕組みを上手に使うと、設定値にまつわるイベントに対してリアクティブに反応できるようになるので、ユーザーの利便性を高めることができます。アドオンを作成される際は、ぜひお試しください。

*1 どのような場合にこのフラグを利用するかは https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Weak_reference を参照ください。

タグ: Mozilla
2017-11-14

Gecko Embeddedプロジェクト 11月時点のステータス

はじめに

2017年7月6日の記事で紹介した通り、クリアコードは組み込みLinux向けにMozilla Firefoxを移植するプロジェクトGecko EmbeddedWebDINO Japan(旧Mozilla Japan)様と共同で立ち上げ、開発を進めております。Yoctoを使用してFirefoxをビルドしたりハードウェアクセラレーションを有効化する際のノウハウを蓄積して公開することで、同じ問題に悩む開発者の助けになることを目指しています。

その後も継続的に改善を進めており、いくつか進展がありましたので、11月時点のステータスを紹介します。

現在のステータス

ターゲットボードの追加

7月時点では、Firefox ESR52のビルド及び動作はRenesas RZ/G1E Starter Kitのみで確認していましたが、その後iWave RainboW-G20D Q7および Renesas R-Car Gen3でも同様に確認しています。ビルド方法は以下のページにまとめてあります。

Wayland対応のバグ修正

Firefoxは正式にはWaylandをサポートしていませんが、Red HatのMartin Stransky氏がWaylandへの移植作業を行っています。 Stransky氏のパッチはFirefoxの最新バージョンを対象としていますが、Gecko Embeddedプロジェクトではこのパッチを52ESRのツリーに移植した上で、安定化作業を進めています。 本プロジェクトで発見した問題や、作成したパッチはStransky氏に随時フィードバックしています。

最近では以下のような報告を行っています。

WebRTC対応

Wayland版FirefoxではWebRTCでビデオチャットを行おうとするとクラッシュする問題が存在していましたが、この問題を修正し、WebRTCが利用できるようになりました。

WebGL対応

7月時点ではドライバに絡む問題を解決できていなかったためビルド時点でのWebGL有効化手段を提供していませんでしたが、その後問題を解決できたため、パッチを更新しYoctoレシピにビルドオプションを追加しています。

WebGLを有効化するには、ビルド時に以下の設定をYoctoのlocal.confに追加して下さい。

PACKAGECONFIG_append_pn-firefox = " webgl "

また、今回新たに対象ボードとして加えたR-Car Gen3は非常に強力なため、フルHDの解像度でもWebGLが快適に動作することを確認しています。

Canvas 2D Contextのアクセラレーション

WebGLと同様に、Canvas 2D Contextのアクセラレーションについてもドライバに絡む問題を解決できたため、Yoctoレシピにビルドオプションを追加しています。

アクセラレーションを有効化するには、ビルド時に以下の設定をYoctoのlocal.confに追加して下さい。

PACKAGECONFIG_append_pn-firefox = " canvas-gpu "

まとめ

Gecko Embeddedプロジェクトの2017年11月現在のステータスについて紹介しました。 安定化や、さらなるパフォーマンス改善、他のSoCのサポートなど、やるべきことはまだまだ残されていますので、 興味がある方は協力して頂けるとありがたいです。問題を発見した場合はGecko EmbeddedプロジェクトのIssueページに報告して下さい。

タグ: Mozilla
2017-11-09

Firefox ESRの独自ビルドの作成方法(2017年版)

2015年にWindowsでのFirefoxの独自ビルドの作成方法をご紹介しましたが、2017年現在では状況が若干変わっています。この記事では、2017年10月現在においてWindows用にFirefox ESRの独自ビルドを作成する手順をご紹介します。

なお、この記事ではFirefoxの改造の方法は扱っていません。「Firefox」というブランド名を除去した改造版を作成する方法については、過去の記事をご参照ください。

情報のありか

Firefoxのビルドに関する情報は、Build Instructionsから辿ることができます。 Windowsでのビルドなら、Building Firefox for Windowsで必要な情報が手に入ります。

ただ、MDNの情報は様々な場合を考慮して網羅的に書かれているため、各項目に複数の選択肢が示されている場合があり、「とりあえずどこから始めればいいのか?」が若干分かりにくい構成となっています。 そこで、この記事では「Firefox ESRをビルドする」という場面に限定して一通りの流れをご紹介してみます。

ビルド環境の構築

現在新規に導入可能なWindows環境はWindows 10である場合がほとんどです。この記事では、Windows 10 Creators Updateまたはそれ以降のバージョンをビルド環境として想定します。 (検証はWindows 10 Pro 1703 (Creators Update) 15063.632で行っています。)

Firefoxのビルドにあたっては、HDDとメモリに十分な余裕があることが望ましいです。ビルドツール、ライブラリ等のインストール領域用と、リポジトリ自体やビルド中に生成される一時ファイルの分、および仮想メモリのスワップ領域用として、HDDに最低でも15GB程度の空き容量を確保しておくとよいでしょう。また、メインメモリが逼迫しているとビルドに余計に時間がかかります。最低でも4GB程度のメモリがあるとよいでしょう。

Windows上でFirefoxをビルドするために必要なソフトウェアは、すべて無料で入手できます。

  • MozillaBuild
    • 2017年10月現在の最新版は「MozillaBuild 3.0」で、以下の説明はすべてこのバージョンを前提とします。
  • DirectX SDK
    • インストール作業の順番次第では「S1023」という番号を伴ってインストールエラーが表示されることがあります。これは、システムに「Visual C++ 2010 再頒布可能パッケージ」の新しいバージョンが存在する場合に起こることが多いです。一旦「Visual C++ 2010 再頒布可能パッケージ」をアンインストールしてからDirectX SDKをインストールし、完了の後改めて最新の「Visual C++ 2010 再頒布可能パッケージ」をインストールし直して下さい。
    • 古いサポート切れのバージョンのWindows 10においては、「Visual C++ 2010 再配布可能パッケージ」が存在しなくてもエラーコード「S1023」を伴ってインストールに失敗する場合があり、Creators Updateへ更新後は発生しなくなるという結果を得られました。問題との関連性は不明ですが、脆弱性への対応などの観点からも、Windows自体を最新版に更新してから臨むことを強くお勧めします。
  • Windows 10 SDK
  • Visual Studio Community 2015 with Update 3
    • 2017年10月現在のVisual Studioの最新版は「Visual Studio 2017」ですが、公式のビルドはVisual Studio 2015で行われているとのことなので、ここでもVS2015を使うことにします。古いバージョンのダウンロード用ページから、日本語版のインストーラを入手して下さい。(Microsoftアカウントが必要)
    • インストール時の機能の選択で「カスタム」を選択し、「プログラミング言語」→「Visual C++」→「Visual C++ 2015用の共通ツール」にチェックを入れる必要があります。
    • インストールが完了したら、Visual Studioを起動して初期設定を完了させる必要があります。
    • インストールと初期設定を終えたら、累積的なアップデートを適用して更新します。

ソースの入手

Firefoxのソースコードはスナップショットのtarballとしても入手できますが、ここではMercurialのリポジトリをcloneする方法で手順を解説します。

環境構築の際に導入したMozillaBuildはMinGWのコマンドラインコンソールを含んでおり、ビルドの作業はこのコンソールから行います。 C:\mozilla-build\start-shell.bat をダブルクリックしてコンソールを開きましょう。

コンソールを開いたら、リポジトリをcloneするためのディレクトリを用意します。

$ mkdir /c/mozilla-source
$ cd /c/mozilla-source

以下でcloneするリポジトリの中に含まれるファイルは開発用の物のため、Norton Securityなどのウィルス対策ソフトウェアが動作していると、Nortonの誤判定でリポジトリの中のファイルが意図せず削除されてしまうことがあります。ここで作成したディレクトリ(C:\mozilla-source)をウィルス対策ソフトウェアの監視対象にしないように設定しておきましょう。例えばNorton Securityであれば、「設定」→「ウィルス対策」→「スキャンとリスク」→「除外/低危険度」→「スキャンから除外する項目」および「自動保護、SONAR、ダウンロードインテリジェンスの検出から除外する項目」で任意のフォルダをスキャン対象外にすることができます。

次に、Firefoxのリポジトリをcloneします。

Firefoxやその他のMozilla製品のリポジトリはhttps://hg.mozilla.org/で公開されています。 Nightlyの最新版やESR版など、どのバージョンをビルドしたいかによってどのリポジトリをcloneするかが変わります。 ESRを含むリリース版のFirefoxのソースはhttps://hg.mozilla.org/releases/以下にあり、ESRの場合はhttps://hg.mozilla.org/releases/mozilla-esr52のようにバージョン番号を含むパスのリポジトリになっています。

今回はFirefox ESR52.4.1をビルドすることにします。まず https://hg.mozilla.org/releases/mozilla-esr52をcloneします。

$ hg clone https://hg.mozilla.org/releases/mozilla-esr52

リポジトリの規模が数GBと大きいので、cloneにはそれなりの時間がかかります。

Firefox本体のリポジトリをcloneできたら、次は言語リソースのリポジトリをcloneします。 言語リソースのリポジトリは言語ごとに分かれており、ESRを含むリリース版のFirefoxの日本語用言語リソースはhttps://hg.mozilla.org/releases/l10n/mozilla-release/jaです。

$ hg clone https://hg.mozilla.org/releases/l10n/mozilla-release/ja ja

cloneする際に、hgコマンドの3番目の引数としてディレクトリ名を、明示的に言語コード名のjaと指定します。 これは、MozillaBuildでのビルド時には言語リソースが言語コード名に基づいて検索されるためです。

ビルドの準備

ビルドするリビジョンへの切り替え

cloneしたリポジトリは、次のリリースに向けての作業が進行している状態になっています。 Firefox ESR52.4.1のように特定のバージョンをビルドするには、タグに基づいてそのリビジョンに切り替える必要があります。

$ cd /c/mozilla-source/mozilla-esr52
$ hg tags | grep -E -e "FIREFOX_.+_RELEASE" | less

Firefox ESR52.4.1のタグはFIREFOX_52_4_1esr_RELEASEですので、ここにリビジョンを切り替えます。

$ cd /c/mozilla-source/mozilla-esr52
$ hg update FIREFOX_52_4_1esr_RELEASE

言語リソースのリポジトリも対応するリビジョンに切り替える必要がありますが、Firefox 45よりも後のバージョンについては何故か、言語リソースのリポジトリには本体と共通のタグが付けられていません。そこで、Android版FirefoxやThundebrirdのタグ情報を参考にしつつ適切なリビジョンを探すことにします。まず、hg log --graphでコミットグラフを表示します(Webでも同様のコミットグラフを見られます)。

$ cd /c/mozilla-source/ja
$ hg log --graph

Firefox ESRの場合は、ESR版の元になったリリースのタグが属しているコミットグラフの縦線を辿っていき、近いバージョン番号のThunderbirdのタグを探します。すると、THUNDERBIRD_52_4_0_RELEASEというタグの付けられたコミットが見つかります。 これより新しいリビジョンはこの縦線の上には存在しませんので、Firefox ESR52.4.1の日本語リソースはこのリビジョンを使ってビルドしてよいと考えられます。

言語リソースの使用リビジョンを特定できたら、そのリビジョンに切り替えて次の行程に進みましょう。

$ cd /c/mozilla-source/ja
$ hg update THUNDERBIRD_52_4_0_RELEASE
ビルドオプションの指定

ビルド対象のリビジョンをチェックアウトしたら、次は、ビルドオプションを指定します。

ビルドオプションの指定はFirefox本体のリポジトリのトップレベルのディレクトリに.mozconfigという名前のテキストファイルとして保存します。 今回の例ではc/mozilla-source/mozilla-esr52/.mozconfigの位置です。

ビルドオプションは様々な物がありますが、ここでは話を単純化するために、公式のFirefox ESR52.4.1のうちブランディングに関わる部分以外は同一の指定とします。

Firefoxではabout:buildconfigを開くとそのバイナリの元になったビルドオプションの一覧を見ることができます。ただ、実際のmozconfigには、それらに加えて環境変数の指定なども必要です。単にビルドオプションだけを指定した状態だと、Visual Studioのランタイムライブラリがインストーラに含まれないなど、一般ユーザの環境で使用するには不都合があるビルド結果となってしまいます。

ビルド環境が64bit版のWindowsで、Visual Studio Community 2015を使う場合の基本的なビルド設定は、Firefox自体のリポジトリのbuild/win32/mozconfig.vs2015-win64の位置(※今回ビルドしたいのはFirefox ESR52.4.1なので、オンラインで例を見る場合はmozilla-esr52リポジトリの物を参照して下さい)にファイルがあります。

このファイルに書かれている設定内容はMozillaで使用しているビルド環境向けの物なのですが、ここまでの手順通りに必要なソフトウェアを導入した場合、Visual Studioのインストール先パスなどが実際の物と異なっています。 ここまでの手順通りに環境を整えた場合のパスを指定するように改めた上で、Firefox ESR52.4.1のabout:buildconfigに列挙されているオプション群から必要でない物を除外し、ローカライズに必要なオプションを足した物が、以下の例です(行頭の#はコメントアウトです)。

export VSPATH='/C/Program Files (x86)/Microsoft Visual Studio 14.0'

#---------- based on build/win32/mozconfig.vs2015-win64 -------------

if [ -z "${VSPATH}" ]; then
    TOOLTOOL_DIR=${TOOLTOOL_DIR:-$topsrcdir}
    VSPATH="$(cd ${TOOLTOOL_DIR} && pwd)/vs2015u3"
fi

VSWINPATH="$(cd "${VSPATH}" && pwd -W)"

# Windows 10 SDKのインストール先はVisual Studioの配下ではないため、適切なパスを明示的に指定する。
#export WINDOWSSDKDIR="${VSWINPATH}/SDK"
export WINDOWSSDKDIR="/C/Program Files (x86)/Windows Kits/10"
export WIN32_REDIST_DIR="${VSPATH}/VC/redist/x86/Microsoft.VC140.CRT"
# 再配布可能なランタイムライブラリはWindwos 10 SDKに含まれるため、こちらも適切なパスを指定する。
#export WIN_UCRT_REDIST_DIR="${VSPATH}/SDK/Redist/ucrt/DLLs/x86"
export WIN_UCRT_REDIST_DIR="${WINDOWSSDKDIR}/Redist/ucrt/DLLs/x86"

# ビルド過程で使われるツール類の一部はVisual Studio配下ではなくWindows 10 SDKに含まれる。
# また、ランタイムライブラリも一部はWindows 10 SDKに含まれている。
# 前述の通り、Windows 10 SDKの正しいインストール先を参照するように改める必要がある。

# 修正前
#export PATH="${VSPATH}/VC/bin/amd64_x86:${VSPATH}/VC/bin/amd64:${VSPATH}/VC/bin:${VSPATH}/SDK/bin/x86:${VSPATH}/SDK/bin/x64:${VSPATH}/DIA SDK/bin:${PATH}"
#export PATH="${VSPATH}/VC/redist/x86/Microsoft.VC140.CRT:${VSPATH}/VC/redist/x64/Microsoft.VC140.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x86:${VSPATH}/SDK/Redist/ucrt/DLLs/x64:${PATH}"

#export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/10.0.14393.0/ucrt:${VSPATH}/SDK/Include/10.0.14393.0/shared:${VSPATH}/SDK/Include/10.0.14393.0/um:${VSPATH}/SDK/Include/10.0.14393.0/winrt:${VSPATH}/DIA SDK/include"
#export LIB="${VSPATH}/VC/lib:${VSPATH}/VC/atlmfc/lib:${VSPATH}/SDK/lib/10.0.14393.0/ucrt/x86:${VSPATH}/SDK/lib/10.0.14393.0/um/x86:${VSPATH}/DIA SDK/lib"

# 修正後
export PATH="${VSPATH}/VC/bin/amd64_x86:${VSPATH}/VC/bin/amd64:${VSPATH}/VC/bin:${WINDOWSSDKDIR}/bin/x86:${WINDOWSSDKDIR}/bin/x64:${VSPATH}/DIA SDK/bin:${PATH}"
export PATH="${VSPATH}/VC/redist/x86/Microsoft.VC140.CRT:${VSPATH}/VC/redist/x64/Microsoft.VC140.CRT:${WINDOWSSDKDIR}/Redist/ucrt/DLLs/x86:${WINDOWSSDKDIR}/Redist/ucrt/DLLs/x64:${PATH}"

export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${WINDOWSSDKDIR}/Include/10.0.15063.0/ucrt:${WINDOWSSDKDIR}/Include/10.0.15063.0/shared:${WINDOWSSDKDIR}/Include/10.0.15063.0/um:${WINDOWSSDKDIR}/Include/10.0.15063.0/winrt:${VSPATH}/DIA SDK/include"
export LIB="${VSPATH}/VC/lib:${VSPATH}/VC/atlmfc/lib:${WINDOWSSDKDIR}/lib/10.0.15063.0/ucrt/x86:${WINDOWSSDKDIR}/lib/10.0.15063.0/um/x86:${VSPATH}/DIA SDK/lib"

. $topsrcdir/build/mozconfig.vs-common

mk_export_correct_style WINDOWSSDKDIR
mk_export_correct_style INCLUDE
mk_export_correct_style LIB
mk_export_correct_style PATH
mk_export_correct_style WIN32_REDIST_DIR
mk_export_correct_style WIN_UCRT_REDIST_DIR

#--------------------------------------------------------------------

# bug 1356493 の回避のため、Windows 10上ではコマンドの探索先パスを追加する。
export PATH="$PATH:$WINDOWSSDKDIR/bin/10.0.15063.0/x64"
# パスを含む環境変数の内容を変更した後は、ビルドツールの1つである
# 「Pymakeが受け付けるパス形式に変換するために、必ず
# 「mk_export_correct_style 環境変数名」を実行する。
mk_export_correct_style PATH

ac_add_options --enable-crashreporter
ac_add_options --enable-release
ac_add_options --enable-jemalloc
ac_add_options --enable-require-all-d3dc-versions
ac_add_options --enable-warnings-as-errors

# 日本語リソースを使うための指定。
mk_add_options MOZ_CO_LOCALES=ja
ac_add_options --enable-ui-locale=ja
ac_add_options --with-l10n-base=c:/mozilla-source

日本語リソースの参照用に--with-l10n-baseで指定するパスは、言語リソースのリポジトリのパスではなく、その1つ上位のディレクトリのパスです。 MozillaBuildは、ここにMOZ_CO_LOCALESで指定した言語コード名を足したパスの /c/mozilla-source/ja に言語リソースがあることを期待します。

ビルドの実施

準備ができたら、いよいよビルドです。 Firefox本体のリポジトリにcdして、./mach buildを実行すればビルドが始まります。

$ cd /c/mozilla-source/mozilla-esr52
$ ./mach build

ビルドに使用するマシンの性能にもよりますが、現在手に入る一般的な性能のPCであれば1時間以内にはビルドが完了すると思われます。 弊社で検証に使用した環境は以下の性能のホストマシン上の仮想マシンで、仮想環境であることのオーバーヘッドがボトルネックとなった結果、ビルド時間はおよそ130分を要しました。

  • Interl Core i7 3.4GHz
  • 16GB RAMのうち8GBを割り当て

ビルドが完了したら、本当に動作するか確かめてみましょう。 以下のコマンドを実行すると、ビルドされたFirefoxが起動します。 アプリケーション名は「Nightly」になっているはずです。

$ ./mach run

正しく動作することを確認できたら、インストーラを作成しましょう。 これは以下のコマンドで行えます。

$ ./mach build installer

できあがったインストーラは、カレントディレクトリから見てobj-i686-pc-mingw32/dist/install/sea/の位置、フルパスではC:\mozilla-source\mozilla-esr52\obj-i686-pc-mingw32\dist\install\sea\の位置に出力されます。

まとめ

以上、Firefox ESR52.4.1を独自にビルドするための手順を簡単に解説しました。

Firefoxは現在様々な部分の刷新を進めており、現時点での最新の開発版においてはビルドに必要なツールとしてRustやLLVM/Clangのセットアップなども必要になってきています。この記事に記載した手順はあくまでESR52でのものなので、残念ながらそのままでは新しいバージョンのFirefoxには適用できませんのでご注意下さい。これについては、次のESRであるFirefox 59ESRがリリースされた後に改めてフォローアップ記事を公開する予定です。

タグ: Mozilla
2017-10-12

Firefox 57以降での従来アドオン廃止を踏まえたFirefoxの企業利用での対策について

Firefox 57では従来形式のアドオンがすべて無効化され、Firefox 52移行で既に使用可能になっている新方式のアドオンのみが有効になります。 この事がFirefoxの企業・法人利用に与える影響とその対策についてご案内します。

ESR版での影響

Firefoxの製品ラインナップには、約1.5ヶ月ごとに更新されメジャーバージョンが繰り上がっていく一般向けの「通常リリース版」と、1年ほどの間メジャーバージョンが固定されセキュリティアップデートだけが提供され続ける「ESR版」とがあります。

現在の通常リリース版の最新版はFirefox 56.0、ESR版の最新版はFirefox 52.4.0ESRです。 ESR版の次のメジャーアップデートはFirefox 59との同時リリースが予定されており、それまでの間は引き続き52.5.0ESR、52.6.0ESRといったセキュリティアップデートが提供される見込みです。 そのため、ESR版をお使いの場合には今すぐ対策を取る必要はありません。

ただし、Firefox 59ESRでは従来アドオンが無効化されるという事は確定事項なので、いざその時が来ても慌てずに済むように、今のうちから対策を検討しておくのが望ましいです。

アドオンによらないカスタマイズが受ける影響

Firefoxの企業利用においては、管理者が決めた設定を全クライアントに反映するための方法としてMCDが使われる事が多いでしょう。

現時点で判明している限りにおいては、MCDでのカスタマイズはFirefox 57以降のバージョンでも引き続き使用可能と考えられます。

ただし、Firefox 57以降のバージョンでは従来アドオンの無効化に伴い、従来アドオンとの互換性のために残されていたもののメンテナンスの負担となっていた内部的な古い機能・実装の削除が進められています。 MCDでは裏技的に Components.classes[...] といったコードによってFirefoxの内部機能へアクセスすることができていましたが、この方法で呼び出していた内部機能がFirefox 59ESRにおいては削除されている可能性があります。 その場合、代替する方法を調査してMCDの設定ファイルを更新しなくてはなりませんが、場合によっては代替手段がないため設定を諦めなくてはいけないかもしれません。 十分にご注意下さい。

アドオンによるカスタマイズが受ける影響

カスタマイズにアドオンを使用していて、そのアドオンがFirefox 57以降のバージョンに対応していない場合、当然ながらFirefox 59においてもそのアドオンは使用できません。 アドオンをFirefox 57以降の新仕様に対応したバージョンに更新するか、当該アドオンで行っていた変更を代替する他の方法でのカスタマイズに切り替える必要があります。

クリアコードではお客様向けのカスタマイズ用として多数のアドオンを開発・提供しています。 しかしながら、これらの中でFirefox 57以降のバージョンでも使用できるように更新された物はほとんどなく、また更新の可能性も極めて低いと言わざるを得ません。 これは従来のアドオンと新方式のアドオンとの根本的な差異に原因があります。

従来のアドオンはFirefoxの内部に自由に変更を加える事ができ、弊社提供のアドオンの多くは、この性質を使ってFirefoxに「使用を禁止したい機能を無効化・非表示にする」といった変更を加えていました。 (図:従来のアドオンの原理) その一方で、Firefox 57以降で使用可能な新方式のアドオンは、各アドオンを隔離された環境で動作させ、あらかじめ用意されたAPIを経由してのみFirefoxに影響を与えられるという仕組みになっています。 (図:新方式のアドオンの原理) そのため、「行いたい変更を実現するためのAPI」が提供されていない変更は不可能ということになります。

なお、新方式のアドオンで利用できるAPIにはNative Messagingという物もあり、「APIが提供されていない事でもこれを使えば実現できる」という触れ込みとなっています。 ですが、上記の理由でFirefox 57に対応できない弊社製アドオンの多くに対しては、残念ながらこの機能も解決策とはなりません。 Native Messaging Hostはアドオンとローカルアプリケーションとの間で通信ができるという物で、Firefoxの外の世界との連携によってできる事の幅は広がりますが、Firefox内部に対しては依然として何もできないままだからです。

使えなくなるアドオンの代替となるカスタマイズ方法

以上の理由から、アドオンへの依存度が高い場合ほど、Firefox 57以降での変更への対応には注意を要します。 運用上使用を禁止したい機能がある場合には、Firefox上では機能を無効化できないため、別の技術レイヤーで機能を停止・無効化しなくてはなりません。

現在公開中の企業利用向けカスタマイズのまとめでは、目的からカスタマイズの方法を逆引きできる形で資料を整備していますが、中にはカスタマイズにアドオンを必要とする項目も含まれています。 そのため各項目について調査を行い、現時点で代替手法が存在する物についてはその旨を追記し、代替手法が無い物はカスタマイズ項目としては廃止の扱いとするよう内容を更新しました。 (※この資料の内容は随時更新されていますので、必ずその時点での最新版をご参照いただくようご注意下さい。)

ただし、この資料には弊社サポートサービスをご利用になられているお客様の環境で必要となったために調査した範囲のカスタマイズ内容のみが記載されており、それ以外の未知のカスタマイズについては情報がございません。 この資料に記載がないカスタマイズを反映されていて、Firefox 57以降への対応に不安をお持ちの企業担当者様がいらっしゃいましたら、まずはお問い合わせフォームよりご相談下さい。

Firefox 57以降に対応済みのアドオンのご紹介:IE View WE

弊社サポートサービスをご利用のお客様の環境ではアドオン「IE View」の使用頻度が高いものの、このアドオン自体は長らく更新が停止されており、また代替となる他のアドオンでは管理者側で設定を制御できるものが無かったことから、弊社で独自にFirefox 57以降にも対応したクローン版であるIE View WEを開発・公開しています。

従来バージョンのIE Viewとの差異として、URLのリストの形式に互換性が無い点にご注意下さい。 IE View WEでは、URLのリストはFirefoxのアドオンで一般的に使用されるマッチパターンのリストとして解釈されます。 従来バージョンでは http://www.example.com のようにホスト名までのみの指定でも機能しますが、IE View WEではhttp://www.example.com/* のように明示的にワイルドカードを使用してマッチパターンとして記載する必要があります。

Native Messaging Hostのインストール

IE View WEは前述のNative Messaging Hostを使用して外部アプリケーションを起動するという動作を実現しているため、使用にあたっては別途専用のNative Messaging Hostのインストールが必要です。 GitHubのリリース一覧ページからieview-we-host.zipをダウンロードして展開し、install.batを管理者として実行すると実行ファイルがインストールされます。 (一般ユーザーとして実行した場合、そのユーザーでのみ使用可能な状態でインストールされます。ご注意下さい。)

管理者による設定の提供

Firefox 57以降のバージョンで使用可能なManaged Storage機能を通じて、管理者が固定の設定を提供することができます。 GitHubのリリース一覧ページからieview-we-managed-host.zipをダウンロードして展開し、install.batを管理者として実行すると設定ファイルがインストールされます。 (一般ユーザーとして実行した場合、そのユーザーでのみ参照可能な状態でインストールされます。ご注意下さい。) ファイルのインストール前にieview-we@clear-code.com.jsonを編集してdata配下に以下の情報を設定すると、それがそのままIE View WE用の設定となります。

  • ieapp (文字列値, IEの実行ファイルのパス)
  • ieargs (文字列値, IEの起動時に指定する追加の引数)
  • forceielist (文字列値, URLのマッチングパターンの空白文字区切りのリスト)
  • disableForce (真偽値, forceielist`で与えられたリストの無効化)
  • contextMenu (真偽値, コンテキストメニュー項目の有効化)
  • debug (真偽値, デバッグログ出力の有効化)

また、Firefox 56以前のバージョン向けとして、MCDの設定ファイルから設定を読み取る機能もあります。 MCD用設定ファイルの以下の設定項目がある場合、それぞれ対応する設定に反映されます。

  • extensions.ieview.ieapp
  • extensions.ieview.ieargs
  • extensions.ieview.forceielist
  • extensions.ieview.disableForce
  • extensions.ieview.contextMenu
  • extensions.ieview.debug

ただし技術的な制限のため、設定ファイルの配置場所や内容、使用状況によっては設定をインポートできない場合があります。 悪しからずご了承下さい。

まとめ

以上、Firefox 59ESRを見据えたFirefox 57以降のバージョンでのアドオンの仕様変更に伴う企業利用上の注意点とその対策、およびFirefox 57以降でも使用可能なIE ViewのクローンであるIE View WEについてご紹介しました。

クリアコードではFirefoxの企業での利用に際してのカスタマイズのご案内、導入支援、発生したトラブルの原因究明、対策の調査等をサポートサービスとして行っております。 これらの事でお困りの企業担当者様がいらっしゃいましたら、お問い合わせフォームよりご相談下さい。

タグ: Mozilla
2017-10-05

Firefoxで外部アプリケーションを起動するだけのアドオンをGo言語で作る方法と注意点

キーワード:golang, バックグラウンド実行, detach

Firefoxのアドオンは、現在はWebExtensionsというAPI群に基づいて開発するようになっています。 このAPI群には「別のブラウザなどの任意のローカルアプリケーションを直接起動する機能」は含まれておらず、そのようなことをしたい場合にはNative Messagingという仕組みで間接的に実現する必要があります。 これは要するに、「コマンドラインオプションやGUIではなく標準入力から与えられる情報を使って、任意のアプリケーションを起動するランチャー」を開発するということです。 関係を図にすると以下のようになります。

+---------------------+
|       Firefox       |   
|+-------------------+|
||Firefox上のアドオン||
|+-------------------+|
|        ↓↑         |
| <WebExtensions API> |
| (Native Messaging)  |
+--------↓↑---------+
     <標準入出力>
         ↓↑
+---------------------+
|      ランチャー     |
+---------------------+
          ↓
   <システムコール>
          ↓
+---------------------+
|外部アプリケーション |
+---------------------+

このNative Messagingの仕組み自体は、細部を除けばGoogle Chrome用拡張機能での仕組みとほぼ同じ仕様です。 そのためFirefoxとChromeに両対応した実装が既にいくつか存在しており、中には上記のようなランチャーとして振る舞う物もあります。 例えばOpen InというプロジェクトではNode.jsベースで開発されたランチャーアプリケーションの実装を使っています。

この記事では、これと似たような物をGo言語で実装する時の注意点を解説します。

起動したい外部アプリケーションが巻き込みで終了されてしまう問題

Go言語では、外部アプリケーションを起動する方法としてexecパッケージを使うのが一般的です。 このパッケージでは同期実行(外部アプリケーションの終了を待ってから次の処理に進む)のexec.Command().Run()と非同期実行(外部アプリケーションを起動した後、すぐに次の処理に進む)のexec.Command().Start()の2つの機能が提供されています。

例えば、FirefoxのWebページ上のコンテキストメニューに「このページをInternet Explorerで開く」のような項目を追加してそこから別のブラウザを起動するというような場合、exec.Command().Run()でIEを起動するとIEを終了するまでの間ずっとランチャーアプリケーションのプロセスが生き続けることになります。 また、そこから起動されるIEのプロセスはFirefoxから見て孫プロセスという扱いになりますので、うっかりその状態でFirefoxを終了すると、孫プロセスになっているIEまでもがまとめて終了されてしまいます。

ということから、このような場面ではexec.Command().Start()の方を使えばよいと考えられるのですが、実際には期待した通りの結果になりません。 こちらで起動した場合でもIEのプロセスは孫プロセスになってしまい、ランチャーがexec.Command().Start()を実行してIEを起動した後でmain()の最後に到達してプロセスが終了すると(Firefoxがランチャーを終了させると)、やはり孫プロセスのIEまで巻き添えで終了されてしまうのです。

プロセスグループを分けての外部アプリケーションの起動

このような現象が発生するのは、Firefox・ランチャー・孫プロセスとして起動されたIEの全てが同じプロセスグループに属しているからです。 言い換えると、ランチャーが起動する孫プロセスについてプロセスグループを分ければ(プロセスをデタッチすれば)、Firefoxやランチャーが終了した後もIEを動作させ続けられると考えられます。 Go言語の場合、一般的にはこれは以下のようにして実現できます。

...
import (
  "os/exec"
  "syscall"
  "log"
)
...

func Launch(path string, args []string) {
  command := exec.Command(path, args...)

  // Windowsの場合
  command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
  // Linux, maxOS (POSIX)の場合
  // command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

  err := command.Start()
  if err != nil {
    log.Fatal(err)
  }
}

システムコールに渡すパラメータを指定する必要があるため、Windowsとそれ以外の環境とでは書き方が変わってきます。

このようにすることで晴れてプロセスグループが分かれてくれて、ランチャー終了後も外部アプリケーションが生き続けるようになってくれる……と思われたのですが、実際にWindows環境のFirefoxで検証してみたところ、残念ながら期待通りの結果は得られませんでした。

上記のようなコードを使って一般的なコマンドラインアプリケーションとして起動する状態にしたランチャーで試す分には、期待通りの振る舞い(ランチャーの終了後も外部アプリケーションのプロセスが残る)を見せました。 しかし、FirefoxのアドオンからNative Messagingの仕組みを経由して起動されるNative Messaging Hostとしてランチャーを動作させると、依然として外部アプリケーションまで終了されてしまうのでした。

Firefoxに固有の事情

これは、Go言語一般の話や、Google Chromeなどと共通の仕組みとしてのNative Messagingではなく、Firefox固有の事情による現象です。

実はWebExtensionsにおけるNative Messagingの説明に記載がありますが、Windowsにおいてこのようなランチャーから外部のプロセスを起動する際は、CREATE_NEW_PROCESS_GROUPではなくCREATE_BREAKAWAY_FROM_JOBという定数で示されるフラグを指定する必要があります。

Go言語の場合はsyscallモジュールにこの定数の定義が含まれていないため、仕様に基づいて0x01000000という数値を直接記述することになります。

func Launch(path string, args []string) {
  command := exec.Command(path, args...)
  // CREATE_BREAKAWAY_FROM_JOB = 0x01000000
  command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x01000000}
  err := command.Start()
  if err != nil {
    log.Fatal(err)
  }
}

WebExtensionsの元になっているGoogle ChromeのNative Messagingの仕様では特にこのような指定は必要ないため、ChromeとFirefoxの両方に対応したNative Messaging Hostを開発する際には注意が必要です。

このようにフラグを指定して実行すると、無事期待する通りの結果を得ることができました。

(この情報は本記事の初版に対するフィードバックで教えていただきました。ご指摘ありがとうございます。)

有効だった回避策(Windows向け)

結論から述べると、この問題は以下のようなバッチファイルで解決できました。

@ECHO OFF

start %*

これは、自身にコマンドライン引数として渡された内容をそのままコマンド列として非同期に実行するというバッチファイルです。 ランチャーから外部アプリケーションを起動する際に、直接起動せずこのバッチファイル(※正確にはcmd.exe)を介して起動するという使い方をします。

この時の各ソフトウェア同士の関係を図にすると、以下のようになります。

+---------------------+
|       Firefox       |   
|+-------------------+|
||Firefox上のアドオン||
|+-------------------+|
|        ↓↑         |
| <WebExtensions APi> |
| (Native Messaging)  |
+--------↓↑---------+
     <標準入出力>
         ↓↑
+---------------------+
|      ランチャー     |
+---------------------+
          ↓
   <システムコール>
          ↓
+---------------------+
|    バッチファイル   |
+---------------------+
          ↓
   <startコマンド>
          ↓
+---------------------+
|外部アプリケーション |
+---------------------+

単純なのですが、これによってバッチファイルから先のプロセスが別のプロセスグループに分かれるようになり、FirefoxからNative Messaging Hostとしてランチャーを起動した場合でも、Firefoxの終了後も外部アプリケーションのプロセスが残り続けるようになりました。 また、このような動作をさせる場合、ランチャーでバッチファイルを起動する際にはcommand.SysProcAttrの指定はあってもなくても結果は変わりませんでした。

なお、バッチファイルを使うとなるとcmd.exe(コマンドプロンプト)のウィンドウが一瞬表示されるのではないかという懸念もありましたが、実際には特にそのようなこともなく自然に外部アプリケーションが起動しました。 これも、Firefoxがランチャーを起動する際に与えている何らかの指定の影響ではないかと考えられます。

まとめ

以上、Go言語で実装したランチャーを経由してFirefoxから外部アプリケーションを起動する際に、Firefoxが終了した後も外部アプリケーションを起動した状態のままとするためには、バッチファイルを使うとよいCREATE_BREAKAWAY_FROM_JOBフラグの指定が必要であるという注意点をご紹介しました。

環境によってはバッチファイルの実行自体が制限されている場合もあるかもしれません。実際に動作するかどうか、はあらかじめ確認を取っていただくことを強くお勧めします。

FirefoxのWebExtensions APIはGoogle Chromeの拡張機能向けAPIを参考に設計されています。そのためChrome用拡張機能の開発でのノウハウの多くを流用することができ、何か詰まった時は「Chrome用拡張機能ではどうするのが普通なのか?」という観点で調べれば解決策が見つかることが多いです。

しかしながら両者は完全に同一のものではなく、前述のようなFirefoxに固有の注意事項というものもあります。本記事の初版公開時には、オフィシャルのドキュメントにある注意書きを見落としたまま先入観からChrome用拡張機能向けの情報だけを調査していた結果、肝心な情報に全く辿り着けないという結果となっていました。検索も万能ではない(検索語句がずれていると必要な情報に辿り着けない)ために頼りすぎる事・その時得られた結果を過信しすぎる事のリスク、先入観に囚われずにオフィシャルの情報を丁寧に読む事の重要性を改めて実感した次第です。

タグ: Mozilla
2017-09-01

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|
タグ: