「Native Messaging」という技術でChrome拡張機能からpythonスクリプトを動かしました

導入

リンクの動画や音声をmp3でダウンロードするPythonスクリプトを昔作ったのですが、使うたびにURLをCUIにコピペするのがどうも面倒。なので、ブラウザのボタン一つで実行できる機能を実現できないか試しました。

結論から言うと無事動作するものが完成しました。自作拡張機能として元気に動作してくれています。
軽い記録を残しておきます。

語弊がないように正確に表現すると、今回の目的は

  • Pythonスクリプトを実行ファイルに固めてローカルに配置
  • Chrome拡張機能からローカル環境配置の実行ファイルにメッセージを送信し起動、処理実行

の二つです。

Pythonは直接動かせないので実行ファイルにした

Chrome拡張機能は基本的にJavaScript、HTML、CSSで動くようです。Pythonでサーバーのようなものを動かしておけばサーバーと通信した何らかの処理の実装は可能そうですが、拡張機能ごときのために常駐ソフトを増やすのはありえない。

そこで候補として出てきたのが「Native Messaging Host」という技術でした。Chrome拡張機能がPCにインストールされたプログラムと直接やりとりするための仕組みらしい。

要は、pythonを実行ファイルにして叩くことが可能ということ。ほぼ解決も同然です。後は実装していくだけです。

EXEとの通信

単純で、main関数の中でメッセージを受け取るだけです。
stdin経由で引数受取った後は、何も処理の制約はないので適当に処理を実装。
このコードをexeにするわけです。

import sys
import os
import json
import struct

# -----------------------------------------------------------------------------
# Chrome拡張機能と通信するためのヘルパー関数
# -----------------------------------------------------------------------------

def get_message():
    """標準入力からJSONメッセージを読み込む"""
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
        return None
    message_length = struct.unpack("@I", raw_length)[0]
    message_str = sys.stdin.buffer.read(message_length).decode("utf-8")
    return json.loads(message_str)

# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------

def main():
    received_message = get_message()
    url = received_message.get("url")
    media_type = received_message.get("media_type", "audio").lower()
    download_type = received_message.get("download_type", "single").lower()
    # あとは適当に


if __name__ == "__main__":
    main()

Nuitkaでexe化

以前PyInstallerを使ったとき、ファイルサイズが大きめで起動も遅かった記憶があり、あまり印象がよくありませんでした。特に今回は拡張機能から呼び出すたびにexe起動する設計としたので、起動時間はクリティカルです。(exeをループ処理で常駐させてstdin処理するみたいなのも可能かもしれませんが、常時起動アプリになるのが気持ち悪いので却下。)

ここで採用したのは「Nuitka」というツール。まあ普通にpip installで使える類のツールです。実際に作ってみると、47MBほどのexeファイルが完成。クリックすると、たしかになかなかの速度で起動しました。

ちなみに、作ったexeファイルを実行すると高確率でNorton先生が検知してきました。やめてね。

レジストリにネイティブホストを登録

Chromeからローカルのexeを呼び出すときは、どうもレジストリに設定ファイルを登録して使用すればよいようです。

まず、こんな感じの設定ファイルをcom.mycustom.host.jsonみたいな感じで保存。pathにはexeファイルを指定して、allowed_originsには後程作成する拡張機能IDを入れます。あとから入れても全く問題ないので後で戻ってきましょう。

{
  "name": "com.mycustom.host",
  "description": "Downloader Native Host",
  "path": "XXXXX\\MyHost.exe",
  "type": "stdio",
  "allowed_origins": ["chrome-extension://aaaaaaaaaaaaaaaaaaaaaa/"]
}

これをレジストリに登録します。自己責任でね。どうもレジストリ弄るときは手が震えます。
実行ファイル側はたしかこれでOK。

@echo off
SET MANIFEST_PATH=XXXXX\com.mycustom.host.json
SET HOST_NAME=com.mycustom.host
REG ADD "HKCU\Software\Google\Chrome\NativeMessagingHosts\%HOST_NAME%" /ve /t REG_SZ /d "%MANIFEST_PATH%" /f

拡張機能作成

ここからはブラウザからどう呼び出すかという話ですね。
そこまで難しい話ではなくて、以下のようにsendNativeMessage経由でネイティブホストを指定してメッセージを送るだけで実行ファイルに入力を与えつつ起動できるようです。

const NATIVE_HOST_NAME = "com.mycustom.host";

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "start-download") {
    chrome.runtime.sendNativeMessage(
      NATIVE_HOST_NAME,
      request.payload,
      (response) => {
        console.log(`Native host response: ${JSON.stringify(response)}`);
        }
      }
    );
    sendResponse({ message: "Begin download..." });
    return true;
  }
});

まあ、あとは拡張機能の細かい実装なので、いいでしょう。

拡張機能インストール

自作拡張機能を作成したら、zipに固めてchromeに投入。勝手に拡張機能IDが付与されるので、IDを前に作成したcom.mycustom.host.jsonに記入しておきましょう。しないと権限不足で実行されません。

いざ実行!

exeが何かしらの処理をして何かしらの動作をしていれば成功です。めでたしめでたし。
今回私が作成したexeは処理全体で5~8秒ほどで処理が完了します。許容範囲ではないでしょうか。

感想

pythonを使った処理を拡張機能から呼び出せると一気に可能性が広がる!・・・ような、広がらないような・・・
今回は拡張機能側に何も返事をする必要がないので実装が楽でしたが、お互いに通信する必要があるような機能を実装するならまた面倒そうな予感がしますね。あとメンテナンス性がだいぶ悪い。

ちゃんとサーバーとか立てておく気持ちがあれば、拡張機能からローカルLLM叩けたり、いろいろできそうですね。

コメント

タイトルとURLをコピーしました