Google ChromeのGUIから辿る、ドキュメント化されていない仕様の調査 - 2021-07-08 - ククログ

ククログ

株式会社クリアコード > ククログ > Google ChromeのGUIから辿る、ドキュメント化されていない仕様の調査

Google ChromeのGUIから辿る、ドキュメント化されていない仕様の調査

先日、当社の法人向けFLOSSサポート業務の一環として、Google Chromeのドキュメント化されていない仕様について調査のご依頼を頂きました。 具体的には、インストール済みの拡張機能の自動更新について、

  1. 更新を停止する方法はあるかどうか。
  2. 自動更新処理はどの程度の間隔で実行される仕様か。

というお問い合わせでした。 本記事では、これらのお問い合わせに回答するために何をどのように調査したかの紹介を通じて、詳細を把握していないOSSのソースコードの歩き方の例を示してみます。

まずはWeb検索

前者については、「Google Chrome extension update disable」といったキーワードでWeb検索を行ったところ、Chromiumのイシュートラッカー上にあった拡張機能のバージョンを固定できるようにして欲しいという要望が見つかり、そこに記載されている内容から、「現時点では、管理者側の設定でそのようにすることはできない」と判明しました1。 他方、後者については的を射た情報を得られませんでした。

このような場合、当社のFLOSSサポートでは、公開されているソースコードを元に実装を調査することが多いです。 ドキュメントは存在するかどうかが不明なため、最悪の場合は骨折り損のくたびれ儲けとなってしまいますが、実装の中には必ず正解が、もしくは正解に辿り着くためのヒントがあるので、時間さえかければ確実に成果を得られます。 また、その過程で対象のソフトウェアの内部をあちこち見て回ることになるため、次以降に調査をするときのための「土地勘」を養う役にも立ちます。

Chromeの元になるOSSであるChromiumのソースコードは、GitHub上のミラーなどから入手できます。 また、Web経由で利用できるソースコード検索サービス「Chromium Code Search」も公式に運用されています。

ただ、Chrome(Chromium)ほどの規模のソフトウェアともなると、何のヒントもない所から闇雲にソースコードを読み始めたのでは、目的の情報に辿り着くことは非常に困難です。 現実的な時間の範囲内で調査結果を得るためには、何らかの手がかりに基づいて、効率よく情報を探す必要があります。

(以下、本記事ではChromeとChromiumを基本的にほぼ同一と見なし、Chromiumで得られた調査結果をChromeにそのまま適用できる想定で記述しています。)

拡張機能の自動更新の間隔をソースコードから調べる

方針1:予想される設計からアタリを付ける

拡張機能の自動更新処理は、おそらくはタイマーで起動されていると思われます。 ということは、タイマー処理を行っている箇所を特定できれば、そこから必要な情報に辿り着けるかも知れません。

しかしながら、残念ながら現時点で当方には、Chrome内部でそのようなタイマーに基づく処理が一般的にどう実装されているかについての知見がありません。 また、拡張機能の自動更新処理を行っているモジュールがどのような名前なのかも不明です。 これでは、実質的には手がかり無しと変わりありません。

方針2:別の機能からアタリを付ける

そこで、視点を少し変えて、拡張機能の自動更新に近い処理として、ユーザーが任意で拡張機能を更新する機能はないかを調べてみました。 機能を呼び出すUIがある場合、機能の名称や表示されているメッセージやラベルの文字列を手がかりとして利用できます。 その文字列を定義している箇所、その文字列を参照している箇所、という具合に辿っていけば、拡張機能の自動更新処理を行っているモジュールを特定できる可能性が高いでしょう。

Chromeでは、拡張機能の管理画面の上部にある「更新」というボタンをクリックすると、任意のタイミングで拡張機能の更新を確認できるようになっています。 しかし、「更新」(英語版では「Update」)という一語だけでは、ソースコード中を検索しても、無数の無関係の部分が結果に表れてしまいます。

そこで次の手として、先のボタンの近くにある別のボタンやGUI要素の中から、より表示文字列が個性的なものを選び、そちらを起点に調査してみることにしました。 具体的には、「パッケージ化されていない拡張機能を読み込む」(英語版では「Load unpacked」)を使うことにしました。

ボタンのラベル文字列から、自動更新の間隔の定義を見つける

前述のChromium Code Searchで "Load unpacked" を検索2すると、12件の結果が見つかりました。 順番に眺めていくと、どうやら chrome/app/extensions_strings.grdp というファイルの中に含まれる物が、このボタンの表示ラベルの定義である様子が窺えました。 そこで、当該ファイルのその部分の前後を見ていくと、予想通り、先の「更新」ボタンのラベルの定義と思われる箇所が見つかりました。 以下に引用します。

  <message name="IDS_EXTENSIONS_TOOLBAR_UPDATE_NOW" desc="The text displayed in the toolbar to update existing extensions now.">
    Update
  </message>

このファイルの書式は不明ですが、どうやらXML形式らしく、name="IDS_EXTENSIONS_TOOLBAR_UPDATE_NOW" という部分に書かれている内容が、このラベル文字列に対応する内部的な識別子のようです。 そこで、今度はこの識別子で再び検索してみたところ、8件の結果が見つかりました。 すると、chrome/browser/ui/webui/extensions/extensions_ui.cc というファイルの中で、また別の識別子との対応関係が定義されている様子が窺えました。 以下に引用します。

    {"toolbarUpdateNow", IDS_EXTENSIONS_TOOLBAR_UPDATE_NOW},

対応付けられている別の識別子の toolbarUpdateNow で検索してみたところ、5件の結果が見つかりました。 うち1件は先ほどのファイルなので除外できます。 また、うち3件は、中間出力であることを想起させる out/ という文字列をパスの先頭に含んでいたり、Debug という文字列をパスに含んでいたりしているので、これらも除外できそうです。 そうすると chrome/browser/resources/extensions/toolbar.html というファイルだけが残ります。 このファイルの中で検索結果に表れた箇所を、以下に引用します。

    <cr-button id="updateNow" on-click="onUpdateNowTap_"
        title="$i18n{toolbarUpdateNowTooltip}">
      $i18n{toolbarUpdateNow}
    </cr-button>

どうやら、カスタム要素を使ったHTMLのようです。 一般的なイベントハンドラの記述とは書式が異なりますが、on-click="onUpdateNowTap_" という部分がイベントに対応する処理との紐付けのようです。

今度は onUpdateNowTap_ で検索してみたところ、5件の結果が見つかりました。 先ほどと同じ要領で、妥当でないと思われる検索結果を除外すると、chrome/browser/resources/extensions/toolbar.ts というTypeScriptらしきファイルの中の関数定義だけが残ります。 検索結果の箇所を以下に引用します。

  private onUpdateNowTap_() {
    // If already updating, do not initiate another update.
    if (this.isUpdating_) {
      return;
    }

    this.isUpdating_ = true;

    const toastManager = getToastManager();
    // Keep the toast open indefinitely.
    toastManager.duration = 0;
    toastManager.show(this.i18n('toolbarUpdatingToast'));
    this.delegate.updateAllExtensions(this.extensions)
        .then(

ここに書かれている this.delegate.updateAllExtensions() というメソッド呼び出しが、どうやら拡張機能の更新を行うもののようです。

updateAllExtensions で再検索すると10件の結果が見つかり、その中の chrome/browser/resources/extensions/service.ts というファイルのマッチ箇所が、そのメソッドの定義のようでした。以下に引用します。

  updateAllExtensions(extensions: chrome.developerPrivate.ExtensionInfo[]):
      Promise<string> {
    /**
     * Attempt to reload local extensions. If an extension fails to load, the
     * user is prompted to try updating the broken extension using loadUnpacked
     * and we skip reloading the remaining local extensions.
     */
    return new Promise<void>((resolve) => {
             chrome.developerPrivate.autoUpdate(() => resolve());
             chrome.metricsPrivate.recordUserAction('Options_UpdateExtensions');
           })
        .then(() => {

ここに書かれている chrome.developerPrivate.autoUpdate() というメソッド呼び出しが、処理の実態のようです。

そこで、autoUpdate だけで再検索すると、115件の結果が見つかりました。 さすがにここから目的の情報まで辿り着くのは大変なので、developerPrivate も検索語句に加えて再検索すると、今度は結果が19件にまで絞り込まれました。 その中にあった chrome/browser/extensions/api/developer_private/developer_private_api.cc というファイルのマッチ箇所の関数定義部分を、以下に引用します。

ExtensionFunction::ResponseAction DeveloperPrivateAutoUpdateFunction::Run() {
  ExtensionUpdater* updater =
      ExtensionSystem::Get(browser_context())->extension_service()->updater();

ここで ExtensionUpdater という型名(クラス名)が出てきています。 Chromium Code Searchでは、どうやらC++で実装されたコードについては、各識別子から対応する別のコードにジャンプできるようで、この部分のリンクをクリックすると、chrome/browser/extensions/updater/extension_updater.h というヘッダーファイル内の、ExtensionUpdater クラスの定義箇所に辿り着くことができました。

ページ左の一覧から、同じディレクトリー内にあるC++の実装のファイルを見てみると、ExtensionUpdater::ScheduleNextCheck() という、いかにも次の更新処理のタイマーを設定していそうなメソッドの定義がありました。 以下に引用します。

void ExtensionUpdater::ScheduleNextCheck() {
  DCHECK(alive_);
  // Jitter the frequency by +/- 20%.
  const double jitter_factor = RandDouble() * 0.4 + 0.8;
  base::TimeDelta delay = base::TimeDelta::FromMilliseconds(
      static_cast<int64_t>(frequency_.InMilliseconds() * jitter_factor));
  content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
      ->PostDelayedTask(FROM_HERE,
                        base::BindOnce(&ExtensionUpdater::NextCheck,
                                       weak_ptr_factory_.GetWeakPtr()),
                        delay);
}

コメントに Jitter the frequency by +/- 20%. と書かれており、何らかの決まった更新間隔に対してプラスマイナス20%の範囲で揺らぎを持たせている3様子が窺えます。

この揺らぎを持たせる対象の frequency_.InMilliseconds() は、何らかの値をミリ秒単位の数値で取得する物のように読めます。 ということは、メソッドの持ち主である frequency_ が、おそらく「更新間隔」の時間の情報を持っているはずです。 識別子のリンクをクリックするとヘッダーファイル内のフィールド定義箇所に遷移しますが、さらにもう一回 frequency_ をクリックすると、このフィールドに値を代入したり参照したりしている箇所が、ページ下部に一覧表示されます。 そこから代入を行っている箇所にジャンプしてみると、以下のように書かれていました。

ExtensionUpdater::ExtensionUpdater(
    ExtensionServiceInterface* service,
    ExtensionPrefs* extension_prefs,
    PrefService* prefs,
    Profile* profile,
    int frequency_seconds,
    ExtensionCache* cache,
    const ExtensionDownloader::Factory& downloader_factory)
    : service_(service),
      downloader_factory_(downloader_factory),
      frequency_(base::TimeDelta::FromSeconds(frequency_seconds)),
      extension_prefs_(extension_prefs),
      prefs_(prefs),
      profile_(profile),
      registry_(ExtensionRegistry::Get(profile)),
      extension_cache_(cache) {
  DCHECK_LE(frequency_seconds, kMaxUpdateFrequencySeconds);
#if defined(NDEBUG)
  // In Release mode we enforce that update checks don't happen too often.
  frequency_seconds = std::max(frequency_seconds, kMinUpdateFrequencySeconds);
#endif
  frequency_seconds = std::min(frequency_seconds, kMaxUpdateFrequencySeconds);
  frequency_ = base::TimeDelta::FromSeconds(frequency_seconds);
}

最小値が kMinUpdateFrequencySeconds 、最大値が kMaxUpdateFrequencySeconds の範囲に収まるように値を丸めており4、その元になる値は ExtensionUpdater のコンストラクタに5番目の引数で与えられているようです。

ここでコンストラクタ名のリンクをクリックすると、ページ下部に、このクラスのインスタンスを生成している箇所が一覧表示されます。 呼び出し箇所は15箇所ありますが、うち14箇所は単体テストの中のもののようなので、残る1箇所にジャンプしてみます。 すると、以下のように書かれていました。

  // Set up the ExtensionUpdater.
  if (autoupdate_enabled) {
    updater_ = std::make_unique<ExtensionUpdater>(
        this, extension_prefs, profile->GetPrefs(), profile,
        kDefaultUpdateFrequencySeconds,
        ExtensionsBrowserClient::Get()->GetExtensionCache(),
        base::BindRepeating(ChromeExtensionDownloaderFactory::CreateForProfile,
                            profile));
  }

コンストラクタの第5引数は kDefaultUpdateFrequencySeconds という定数になっています。 識別子のリンクをクリックすると定数の定義箇所にジャンプし、以下のように定義されていると分かりました。

// If auto-updates are turned on, default to running every 5 hours.
const int kDefaultUpdateFrequencySeconds = 60 * 60 * 5;

コメントには「自動更新が有効であれば、既定では5時間ごとに更新を行う」とあります。 実際にはすでに見たとおり、ユーザー設定値などを参照している様子は無く、この定数の値がそのまま使用されています。

ここまでで分かったことをまとめると、

  • 拡張機能の自動更新の処理は、約5時間ごとに実行される。
  • 実際の実行間隔にはプラスマイナス20%の揺らぎがある。

ということが言えます。

ここまで分かった時点で、改めて chrome extension update 5 hours といったキーワードで検索してみた所、「How often do Chrome extensions automatically update?(拡張機能の自動更新の間隔はどのくらいか?)」というそのものズバリの質問がStackOverflowにあったことが分かりました。

なお、記事の回答には「extensions-update-frequency というコマンドライン引数で間隔を変更できる」とありましたが、ソースコード上では前述の通り定数をそのまま使っていて、コマンドライン引数で与えられた値を参照している箇所は見当たらなかったので、どうやら、この機能は現在は廃止されているようです。

Chromeの起動から最初の自動更新実施までの時間をソースコードから調べる

ここまでの調査で「自動更新の間隔」は分かりましたが、「Chrome起動から最初の自動更新が実施されるまでの時間」はまだ分かっていません5。 次はその点を調べていきます。

先ほど ExtensionUpdater::ScheduleNextCheck() というメソッドを見つけましたが、同じファイルの中には ExtensionUpdater::CheckNow() という、や ExtensionUpdater::NextCheck()ExtensionUpdater::CheckSoon() といった、「今すぐ実施する」事を示唆する名前のメソッドもあるようでした。 これらのメソッドの呼び出し箇所も調べていくことにします。

ExtensionUpdater::CheckNow() の呼び出し箇所をChromium Code Search上で辿っていくと(辿り方は先ほどと同じなので、詳細は省略します)、

  1. ExtensionService::OnAllExternalProvidersReady()
  2. ExtensionService::CheckForExternalUpdates()
  3. ExtensionService::Init()
  4. ExtensionSystemImpl::Shared::Init()
  5. ExtensionSystemImpl::InitForRegularProfile()
  6. ProfileManager::DoFinalInitForServices()
  7. ProfileManager::DoFinalInit()
  8. ProfileManager::DoFinalInit()
  9. ProfileManager::OnProfileCreationFinished()
  10. ProfileImpl::DoFinalInit()
  11. ProfileImpl::OnLocaleReady()
  12. ProfileImpl::OnPrefsLoaded()
  13. ProfileImpl::ProfileImpl()
  14. Profile::CreateProfile()
  15. ProfileManager::CreateProfileHelper()
  16. ProfileManager::CreateAndInitializeProfile()
  17. ProfileManager::GetProfile()
  18. GetStartupProfile()
  19. CreatePrimaryProfile()
  20. ChromeBrowserMainParts::PreMainMessageLoopRunImpl()
  21. ChromeBrowserMainParts::PreMainMessageLoopRun()
  22. ChromeBrowserMainPartsChromeos::PreMainMessageLoopRun()
  23. BrowserMainLoop::PreMainMessageLoopRun()
  24. BrowserMainLoop::CreateStartupTasks()
  25. BrowserMainRunnerImpl::Initialize()
  26. BrowserMain()

と、最上位の呼び出し元に到達することができました。 この BrowserMain() は、Chromeのプロセスを起動したときに最初に実行される、いわゆるmain関数です。 このことから、以下のことが言えるようです。

  • Chrome起動直後に、拡張機能の自動更新が行われる(場合がある)。

まとめ

Chromeの拡張機能の自動更新が行われる契機について、Chromium Code Searchを使って実装を調査し、以下のことを明らかにしました。

  • 拡張機能の自動更新の処理は、約5時間ごとに実行される。
  • 実際の実行間隔にはプラスマイナス20%の揺らぎがある。
  • Chrome起動直後に、拡張機能の自動更新が行われる(場合がある)。

Chrome(Chromium)のような大規模なプロジェクトでも、「画面上に表示されているメッセージ」や「UIのラベル文字列」などのような、手がかりになる確実な情報があると、比較的簡単に、ソースコードから必要な情報を見つけることができます。 また、そうして調べて分かったことが、さらに次の調査の手がかりにもなります。

今回の調査ではChromium Code Searchの高度な支援にかなり助けられました。 FirefoxやThunderbirdにも、同様のSearchfoxというサービスがあり6、こちらも、ソースコードの静的な解析結果に基づいて、同名のクラスやメソッドがあっても、適切な呼び出し元を辿っていきやすくなっています。 GitHubにホストされているプロジェクトであれば、ページ上部の検索窓からソースコード内の文字列を検索できるようになっています。 皆さんも、「このOSSの仕様はどうなっているんだろう?」「このOSSの動作はどう決定されているんだろう?」と疑問を持ったときは、それをきっかけにして、ソースコードの調査に挑戦してみてはいかがでしょうか。

また、今回のような調査を行う上では、ソースコードがある程度「読みやすく」書かれていることが、調査の効率に影響します。 後半の調査では敢えてmain関数まで辿りましたが、場合によっては、「このような名前のメソッドが見つかったから、もう調査を終えて良さそうだ」といった判断を行うこともあります。 このように「読む」ものとしてソースコードに接する機会が増えると、「読みやすい」コードとはどういうものを言うのかを、逆説的に実感できるのではないでしょうか。 そういった学びを得られる機会にもなりますので、OSSのソースコードをまだ読んだことが無い方は、ぜひ勇気を出して、ソースコードを読むことに挑戦してみることをおすすめします。

  1. どうしてもそのようなことを行いたい場合、対象の拡張機能自体をフォークして、独自の更新情報を提供するURLを、更新情報の参照用URLとしてマニフェストファイル内に記載する必要があります。

  2. 複数語句を入力すると、ファイル内の離れた位置にそれらが登場するケースまで結果に表れてしまうため、二重引用符で括っているのがポイント。

  3. 乱数を0.4倍すると、0から0.4までの範囲でランダムな小数を得ることができ、そこに0.8を足すことで、0.8から1.2までの範囲のランダムな小数となります。これを固定の更新間隔に対してかけ算すれば、0.8倍から1.2倍の範囲で揺らぎが生じることになります。

  4. それぞれの定数を確認すると、最小30秒、最大7日間となっていました。

  5. 例えば、Firefoxの拡張機能の自動更新処理の場合は、途中でFirefoxが終了・再起動された場合でも、前回実施から最低24時間が経過するまでは次の自動更新を行わない設計になっています。

  6. Chromium Code Searchではできないようですが、Searchfoxではブランチを指定することで、Firefoxの過去のバージョンを対象にした検索も可能です。Chromeのソースコードについて特定の過去バージョンを対象に検索するためには、リポジトリから過去のリビジョンのソースコードをダウンロードしてローカルで検索する必要があるようです。