テックタッチアドベントカレンダー6日目の記事です。
エンジニアの roki です。朝ラン愛好家なのですが、今年は初秋にひざを故障してしまい3ヶ月ほどお休みの後、全快とは言えないものの最近復活しました。気がつけば木枯らしが身にしみる季節。体は大事にしたいものです。
この記事では、ブラウザ拡張がPC内の他のプロセスと通信を行うための技術 Native Messaging の概要と、これを使ったデモアプリケーションの紹介をします。
Native Messaging とは
Native Messaging を用いると、Web ブラウザとは別に、ユーザーのPCにインストールされたネイティブアプリケーションとブラウザ拡張の間でメッセージ交換を行うことができます。つまり、Web ブラウザに用意された API ではアクセスできないハードウェアなどのリソースに対して、ブラウザ拡張からアクセスできるようになります。過去に何故お役所ってオワコンIEが大好きなの?という解説記事があり、その中でも触れられている技術です。今では Chrome, (Chromium ベースの)Edge, Firefox, Safari でも使えるようになっています。Chrome と Edge はベースが同じ Chromium なので、片方で動けば、後述する manifest の配備パスを変えるくらいでほぼ何も考えずに両方で扱うことができます。
Chrome, Firefox, Safariについては実装サンプルへのリンクが各ページにあります。
ここで用いるネイティブアプリケーションは Native Messaging の manifest ファイルにより指定され、Web ブラウザに認識されます。Web ブラウザ、ネイティブアプリケーション、manifest ファイルの関係は以下のとおりです(図は https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Native_messaging からの抜粋です)。My_WebExtension がブラウザ拡張、My_Native_app がネイティブアプリケーション、My_Native_app.json が manifest ファイルになります:
manifest ファイルの準備
Native Messaging を扱うにあたり2つの manifest ファイルを扱います。1つはブラウザ拡張自体の manifest.json で、もう1つは native messaging host (Native Messaging で用いるネイティブアプリケーション)をどうやって起動するかを記した設定ファイルです。
ブラウザ拡張自体の manifest.json には、Native Messaging に対する permission が必要です:
{ ... "permissions": [ "nativeMessaging" ], ... }
Native messaging host のための manifest ファイルは、例えば Chrome / Edge だと以下のようになります:
{ "name": "com.github.ihiroky.system_monitor", "description": "System Monitor", "path": "/home/hiroki/projects/chrome-extension-system-monitor/dist/collector", "type": "stdio", "allowed_origins": [ "chrome-extension://idcccdapgdfdoknajemcliejfklkbhjf/" ] }
各項目の概要は以下のとおりです:
プロパティ名 | 説明 |
---|---|
name | ブラウザ拡張から native messaging host を呼び出すときに使う名前。 |
description | Native messaging host の概要説明。 |
path | Native messaging host 実行ファイルへのパス。 |
type | Native Messaging の通信方式。今は stdio (標準入出力)のみ。 |
allowed_origins | この native messaging host を呼び出すことができる拡張のリスト。Firefox では allowed_extensions でブラウザ拡張のマニフェスト内 applications.gecko.id を指定。 |
詳細な説明は、以下のドキュメントを参照してください: https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests
この manifest ファイルは Windows ではレジストリ経由でパス指定、Mac や Linux では所定の位置に配置することで、Web ブラウザは native messaging host を見つけることができるようになります。詳細は下記URLを見てください: https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-location https://docs.microsoft.com/en-us/microsoft-edge/extensions-chromium/developer-guide/native-messaging?tabs=linux#step-3—copy-the-native-messaging-host-manifest-file-to-your-system https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location
ブラウザ拡張内の処理
ブラウザ拡張がネイティブアプリケーションと通信するためには、background と content_script 間の message passing と同様に、コネクションベースの通信とコネクションレスの通信の2方式があります。
コネクションベースの場合は、ブラウザ拡張内で runtime.connectNative() により runtime.Port オブジェクトを生成すると、ネイティブアプリケーションが起動します。runtime.connectNative() の引数に前述の manifest に記述した name を指定することで、起動対象のアプリケーションを指定します。この runtime.Port オブジェクトの postMessage(), onMessage に登録したリスナーを用いてネイティブアプリケーションとのメッセージ送受信を行い、また disconnect() を呼ぶとネイティブアプリケーション終了し、接続も終わります。
// com.github.ihiroky.system_monitor という名前のついた native messaging host を起動し、接続する const port = chrome.runtime.connectNative("com.github.ihiroky.system_monitor"); // Native messaging host からメッセージを受信する port.onMessage.addListener((response) => { console.log("Received: " + response); }); console.log("Sending: ping"); port.postMessage("ping"); // 接続を切断し、native messaging host を終了する setTimeout(() => { port.disconnect() }, 3000);
コネクションレスの場合は、 runtime.sendNativeMessage() によりメッセージの送受信を行い、ここでも前述の manifest に記述したname を指定することで、対象のネイティブアプリケーションを指定します。この場合、メッセージの送受信が行われている間だけネイティブアプリケーションが起動し、runtime.sendNativeMessage() の処理が完了するとネイティブアプリケーションは終了します。
// com.github.ihiroky.system-monitor という名前のついた native messaging host // を起動し、メッセージを投げて受け取ったのち native messaging host を終了する console.log("Sending: ping"); chrome.runtime.sendNativeMessage("com.github.ihiroky.system-monitor", "ping") .then(response => { console.log("Received " + response); }).catch(error => { console.log(`Error: ${error}`); });
なお、native messaging host との通信はすべて background に記述する必要があります。また、メッセージは JSON として扱える JavaScript の値である必要があります。
Native messaging プロトコル
ブラウザ拡張とネイティブアプリケーションの間でメッセージを交換するにあたり、ネイティブアプリケーションとの通信プロトコルに UTF-8 エンコードされた JSON が使われています。その先頭にエンコードされた JSON の長さを符号なし32ビット整数(native byte order、PCのCPUであればほぼリトルエンディアンかと思います)を付け加えたものが、1つのメッセージとしてやりとりされます。
1メッセージあたりの最大サイズに制限があり、ブラウザ拡張からネイティブアプリケーションへのメッセージが4GB、ネイティブアプリケーションからブラウザ拡張へのメッセージが1MBになっています。大きなデータを扱いたい場合は注意が必要です。
ネイティブアプリケーションは、標準入力からこのメッセージを読み取り、ブラウザ拡張へメッセージを送信する場合はこのフォーマットに従ったメッセージを標準出力へ書き出すことで通信が成立します。golang で表現すると以下のようになります(みやすさのためエラーハンドリングは省略しています)。
// メッセージの受信 import "bufio" import "encoding/binary" import "io" ... stdin := bufio.NewReader(os.Stdin) var length uint32 binary.Read(reader, binary.LittleEndian, &length) buf := make([]byte, length) io.ReadFull(reader, buf)
// メッセージの送信 import "bufio" import "encoding/binary" ... stdout := bufio.NewWriter(os.Stdout) length := len(payload) // payload: utf8エンコードされたJSONが詰まっている[]byte binary.Write(stdout, binary.LittleEndian, int32(length)) for head := 0; head < length; { n, _ := stdout.Write(payload[head:]) head += n } stdout.Flush()
デバッグ時に気をつけること
基本的にエラーはブラウザ拡張のコンソールに出るのでこれを確認していきます。具体的にどのようなメッセージが出るかは Web ブラウザによるので各ドキュメントを参照すると良いです。大抵の状況については網羅されているように思えるので、うまく通信できない自体に陥った場合は、あれこれ探し回る前に下記ドキュメントに目を通しておくことを とても強く お勧めします。
https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-debugging https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#troubleshooting
また、native messaging host の標準エラー出力やその他拡張関連エラーは web ブラウザー自体のログにも出力されます。デバッグを始める前に web ブラウザをターミナルから起動して、場合によってはデバッグログを有効化しておくと有用な情報が得られるかもしれません。Chrome, Edge の場合は以下の文書が参考になります。
https://www.chromium.org/for-testers/enable-logging
デモアプリケーション
上記を踏まえデモアプリケーションを作りました。ハードウェアをブラウザから触ってみると面白いかと思ったのですが、適当なハードウェアがなかったので CPU、メモリ、ストレージI/O、ネットワークI/O の様子を収集してみました。突貫工事につき、Linux かつ Chromium 系のブラウザでしか動かない、かつ値に怪しさがただよっているものの値が収集できている様子は確認できます。