runtime.onStartup を有効無効切り替え時にも呼び出す

はじめに

Manifest V3 で拡張機能のバックグラウンド処理は、無期限に生存できなくなりました。

それに伴い、chrome.runtime.onInstalled/chrome.runtime.onStartupが重要な処理を担うようになりました。ただし、これには問題があります。拡張機能の有効無効切り替えを認識できないことです。

無効状態の拡張機能が有効状態に遷移した時、chrome.runtime.onStartupは発火しません。この問題を解決する必要があります。

問題

background.jschrome.runtime.onInstalled.addListener(onInstalled);
chrome.runtime.onStartup.addListener(onStartup);

拡張機能の有効無効切り替え時にonStartupが動作しません。

解決策1

background.jschrome.runtime.onInstalled.addListener(onInstalled);
//chrome.runtime.onStartup.addListener(onStartup);
onStartup();
// 備考:Service Worker の復帰毎に処理を実施します

background.js の起動毎にonStartup処理を実行します。
愚かな行為であることは認識していますが、これはある程度有効に機能します。

解決策2

background.jschrome.runtime.onInstalled.addListener(onInstalled);
chrome.runtime.onStartup.addListener(onStartup);
chrome.management.onEnabled.addListener((info) => {
  if (info.id === chrome.runtime.id) {
    onStartup();
  }
  // 備考:起動時にブラウザ内の有効な拡張機能毎に呼び出されます
  //       もちろん、有効無効切り替え時にも呼び出されます
});

この方法は、有効無効の切り替えに対して有効に機能しますが、問題を抱えています。

onStartupは、ブラウザ起動時に2回呼び出されます。chrome.runtime.onStartup,chrome.management.onEnabledの両方からonStartupが呼び出されることが原因です。

management権限を必要とします。chrome.managementへアクセスするために追加で権限が必要になります。この権限は、ブラウザ上の他の拡張機能の状態を取得する権限です。これは、拡張機能に分不相応な権限を付与することになります。

参考

解決策3

background.jsconst startup = async () => {
  const sessionStorage = await chrome.storage.session?.get({startup:false});
  if (!sessionStorage?.startup) {
    await chrome.storage.session?.set({startup:true});

    await onStartup();
  }
  // 備考:有効無効時は chrome.runtime.onStartup が呼ばれない対策
  // 備考:chrome.storage.session
  //       更新時、内容は消える
  //       有効無効時、内容は消える
  //       Service Worker 復帰時、内容は残る
  //       対応時期:Chrome 102, Firefox115
};
chrome.runtime.onInstalled.addListener(onInstalled);
//chrome.runtime.onStartup.addListener(onStartup);
startup();

新機能のchrome.storage.sessionを使用する方法です。storage権限を必要としますが、一般的にこの権限が問題となることはないはずです。

参考

最終案

background.jslet startupingPromise = null;
const startuping = async () => {
  const sessionStorage = await chrome.storage.session?.get({startup:false});
  if (!sessionStorage?.startup) {
    await chrome.storage.session?.set({startup:true});

    const manifest = chrome.runtime.getManifest();
    const localStorage = await chrome.storage.local.get({extension_version:''});
    if (manifest.version != localStorage.extension_version) {
      await chrome.storage.local.set({extension_version:manifest.version});

      await onInstalled();
    }
    // 備考:無効状態の拡張機能が更新しても chrome.runtime.onInstalled が呼ばれない対策(Chrome 限定?)
    //   see https://issues.chromium.org/issues/41116832

    await onStartup();
  }
  // 備考:有効無効時は chrome.runtime.onStartup が呼ばれない対策
  // 備考:chrome.storage.session
  //       更新時、内容は消える
  //       有効無効時、内容は消える
  //       Service Worker 復帰時、内容は残る
  //       対応時期:Chrome 102, Firefox115
  // 備考:onInstalled, onStartup の実行順・実行タイミングを保証します
  //       標準機能では、 onStartup > onInstalled の順で実行されることがあります
  //       また、並行(非同期)に実行されることがあります
};
const startup = async () => {
  if (startupingPromise) {
    await startupingPromise;
  } else {
    startupingPromise = startuping();
    await startupingPromise;
    //startupingPromise = null;
  }
  // 備考:並行実行を阻止する。完了を待機する。シングルトン
};
chrome.runtime.onInstalled.addListener(startup);
chrome.runtime.onStartup.addListener(startup);
startup();
// 備考:chrome.runtime.onStartup を呼び出さないと、
//       起動時に background.js が動作しない対策(Firefox 限定?)

onInstalledは、バージョンアップ毎に呼び出されます。
 更新毎には、呼び出されません。
 通常は、問題になりませんがデバッグで問題になることがあります。
※次の要素名は変更可能です。
 chrome.storage.session: {startup}
 chrome.storage.local: {extension_version}

備考

onStartupは、次の機能の初期化に必要になります。

chrome.action.setPopup({popup});
// 備考:クリックとポップアップ両方に対応する場合、設定が必要になります

chrome.contextMenus.create();
// 備考:起動時は、メニューがないため、作成する必要があります

備考:無効状態で更新されても更新イベントが発火しない

無効状態の拡張機能が更新してもchrome.runtime.onInstalledが呼び出されない。
(Chrome 限定?)

備考:スタートアップキャッシュクリア済みの起動時

スタートアップキャッシュクリア済みの起動時は、 Firefox のコンテキストメニューの初期化が必要になる。だが、chrome.runtime.onStartupなしの起動時は、 background.js が呼び出されない。(スタートアップキャッシュクリアなしの Firefox 起動時は、コンテキストメニューがあるため、問題とはならない)

  • Profile folder(xxx.default)/startupCache (手動削除)
  • [about:support] > [Clear startup cache..]