RMSでメールアドレス検索するThunderbirdのWebExtensions版アドオンを作った話 その2

スタッフブログ

皆様どうも、こんにちは!
こまりの自称ソフトウェアエンジニア、桑木です。

前回の記事では、

  1. ThunderbirdからURLを別アプリケーションに伝える
  2. 別アプリケーションではOSのデフォルトブラウザでURLを開く
  3. ブラウザでメールアドレス検索を実行する

この3段階の手順のうち、1番目を実行するThunderbirdアドオンの作成についてご紹介しました。

今回は、2番目のアドオンからURLを受け取ってOSのデフォルトブラウザで開くアプリケーションの作成についてご紹介します。

ネイティブアプリケーション

ここで言うネイティブアプリケーションとは、OSで直接動作する普通のアプリケーションのことです。WebExtensionsのNative messagingは名前の通り、OSネイティブのアプリケーションとメッセージをやりとりするためのAPIということです。

Windowsの場合、アプリケーションには拡張子がexeの実行ファイル形式を用いるのが一般的です。ここでもそのexeファイルを作成していきます。

入出力形式

Native messagingでは、アプリケーションとのメッセージのやりとりに一般的な標準入力及び標準出力を使用します。詳しいことはここに書いてありますが、メッセージ本体はUTF8エンコードのJSONフォーマットで、メッセージ本体の前に32bit整数でメッセージサイズがくっつくようです。

バイナリを扱う必要はありますが、アプリケーション側の入出力も比較的単純な処理で済みそうですね。

Windowsでデフォルトブラウザを開く

あまりWindowsネイティブなアプリケーションの開発経験がないのでベストな方法は知りませんが、ネイティブアプリならWin32APIを直接呼び出せるのでShellExecuteにURLを渡すだけで良さそうです。

ウェブを検索してみると、Win32APIを直接呼び出さない方法としてrundll32.exe url.dll,FileProtocolHandler URIというコマンドを実行する方法も散見されます。前述のShellExecuteも結局はshell32.dllの関数ですが、rundll32.exeは呼び出せる関数に制限があるのでFileProtocolHandlerを利用するようです。

折角のネイティブアプリなので前者の方法がスマートで良さそうですが、結局は後者のFileProtocolHandlerをrundll32経由で実行する方法を採用しました。

nexe

さて、そんなに難しそうな処理でもないのでC++とかC#あたりでゴリっと作っても良かったのですが、UTF8とかJSONとか面倒な予感がしたのと、MDNのNative messagingのページにNode.js用のサンプルコードが記載されていたので、Node.jsで作成してnexeで実行ファイル化することにしました。

Node.jsからでもWin32APIは呼び出せるみたいですが、それはそれで面倒そうなので前述の通りrundll32.exeを実行するという安易な方法を採用しました。既に動いて使用しているアドオンの改修に時間を掛けたくなかったという事情もあります。

// package.json
{
  "name": "native-wrapper",
  "version": "1.0.0",
  "description": "native-wrapper",
  "main": "src/index.js",
  "scripts": {
    "clean": "rimraf ./dest",
    "nexe": "nexe -t x64-8.0.0 --output ./dest/native-wrapper.exe",
    "cp": "cpx ./assets/** ./dest/",
    "candle": "cd dest && candle native-wrapper.wxs && light -o ..\\..\\native-wrapper.msi native-wrapper.wixobj",
    "build": "npm-run-all clean nexe cp candle"
  },
  "keywords": [],
  "author": "Komari co. ltd.",
  "private": true,
  "license": "UNLICENSED",
  "devDependencies": {
    "cpx": "^1.5.0",
    "nexe": "^4.0.0-beta.14",
    "npm-run-all": "^4.1.5",
    "rimraf": "^3.0.2"
  }
}
// src/index.js
const ChildProcess = require('child_process');

process.stdin.on("readable", () => {
    var input = [];
    var chunk;
    while ((chunk = process.stdin.read())) {
        input.push(chunk);
    }
    input = Buffer.concat(input);

    if (!input.length) {
        return;
    }

    var msgLen = input.readUInt32LE(0);
    var dataLen = msgLen + 4;

    if (input.length >= dataLen) {
        var content = input.slice(4, dataLen);
        var json = JSON.parse(content.toString());
        handleMessage(json);
    }
});

function sendMessage(msg) {
    var buffer = Buffer.from(JSON.stringify(msg));

    var header = Buffer.alloc(4);
    header.writeUInt32LE(buffer.length, 0);

    var data = Buffer.concat([header, buffer]);
    process.stdout.write(data);
}

process.on("uncaughtException", (err) => {
    sendMessage({ error: err.toString() });
    process.exit(1);
});

function handleMessage(data) {
    const cp = ChildProcess.spawn('rundll32.exe', ['url.dll,FileProtocolHandler', data.uri]);
    cp.on('exit', code => {
        sendMessage(code);
        setTimeout(() => {
            process.exit(code);
        }, 5000);
    });
}

ほぼMDNのサンプルコードなので特に説明することもありませんが、メッセージを受け取ったらNode.jsのchild_process.spawn()rundll32.exeを実行しているだけです。Node.jsのBuffer#toString()はBufferのバイナリ列をデフォルトでUTF8として扱います。

このアプリケーションは、Native messagingのconnectNative()ではなくsendNativeMessage()から実行されるので、必要な処理が終わって形ばかりのレスポンスメッセージを返したら終了しています。どういう理屈かわかりませんが、ChildProcessのexitイベントを受け取ってすぐにprocess.exit()するとブラウザが開かれないことが多いので5秒程度のウェイトを入れています。

上のpackage.jsonを使うならnpm iでnexe等をインストールしてnpm run nexeで実行ファイルを作成できます。

npm-scriptsのnexeではnexe -t x64-8.0.0と対象のバージョンを指定していますが、Node.jsのバージョンによっては指定しないと正常にnexeが実行されません。以前、nexeを別件で使用した際は特に対象のバージョンを指定しなくても動作したので、これに気付くまで時間を無駄に使ってしまいました。

ネイティブアプリケーションのマニフェスト

アドオンの作成ではアドオン自身の情報を記述するためにmanifest.jsonを作成しました。Native messagingで呼び出されるには、ネイティブアプリケーションにも情報を記述したjsonファイルを作成する必要があります。

マニフェストファイル

Linuxではこのファイルを配置する場所とファイル名に指定がありますが、Windowsではなんでも良いので、とりあえずこちらもmanifest.jsonという名前で作成して実行ファイルと同じディレクトリに配置することにします。

// assets/manifest.json
{
    "name": "jp.komari.native_wrapper",
    "description": "ファイルやURIを関連付けされたアプリで開く",
    "path": "native-wrapper.exe",
    "type": "stdio",
    "allowed_extensions": ["rmssearch@komari.jp"]
}

nameはNative messagingのsendNativeMessage()で指定されるアプリケーション名を設定します。

pathには実行ファイルのパスを記述します。Windowsの場合は相対パスで良いので、実行ファイルと同じディレクトリにこのjsonファイルを配置するなら、実行ファイルのファイル名を記述するだけになります。

そしてallowed_extensionsに、このネイティブアプリケーションを呼び出しても良いアドオンのIDを記述します。ここでは前回作成したアドオンのmanifest.jsonに記述したapplications.gecko.idの値を設定します。

レジストリへ登録

Native messaging APIからこのマニフェストファイルを参照するためには、指定したアプリケーション名のマニフェストファイルがどこにあるかブラウザやメールソフトに教える必要があります。

Linuxでは特定のディレクトリにアプリケーション名でjsonファイルを配置すれば良いのですが、Windowsの場合はレジストリの特定のパスにアプリケーション名のキーを作成して、その値でマニフェストファイルのパスを指定する必要があります。

MDNにはFirefoxの場合のレジストリキー名が書かれていますが、Thunderbirdも同じでHKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\アプリケーション名規定の値にマニフェストファイルのパスを設定します。HKEY_LOCAL_MACHINEHKEY_CURRENT_USERでも良いようです。

ところで、64bit版のWindowsには互換性のために32bitアプリケーション用のレジストリも存在します。Thunderbirdが64bit版の場合はもちろん64bit用のレジストリを参照します。MDNには32bitと64bit両方のレジストリを参照する、というようなことが書かれていますが、対象のThunderbirdが64bitなら64bitのレジストリに書き込んだ方が確実でしょう。

ネイティブアプリケーションのインストール

作成したexeファイルとマニフェストファイルをドライブのどこかに配置して、そのマニフェストファイルのパスをレジストリに登録することで、めでたくThunderbirdのアドオンから別アプリケーションを呼び出すことに成功しました。

しかし、このアドオンを使用するのは私だけではありません。他のコンピュータにも同様の設定をする必要があります。できればダブルクリック1回で自動的に設定できるようにして、使用者自身で導入してもらいたいところです。

それにはバッチファイルを使用する方法も考えられますが、インストール先をどうするかとか、もしアンインストールするとしたらレジストリをどうするか、といった問題があります。そこで近頃のWindowsアプリ配布でよく見かけるmsi形式のインストーラを作成することにしました。

WiX Toolset

WiXはWindowsのインストーラを作成するためのツールです。今回始めて使用するので詳しいことは知りませんが、ファイルやレジストリをどう配置したいかという情報をXMLで記述することで、それに応じたインストーラを作成してくれるようです。

// assets/native-wrapper.wxs
<?xml version='1.0' encoding='utf-8'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
    <Product
        Name='Thunderbird RMS検索アドオン用 native-wrapper 1.0.0'
        Id='3D8B39E1-2801-46E9-9143-E923A583A5EF'
        UpgradeCode='77758AD1-89A5-41B0-A157-167A36150944'
        Language='1041' Codepage='932'
        Version='1.0.0' Manufacturer='Komari co. ltd.'
    >
        <Package
            Id='*' Keywords='Installer'
            Description="Thunderbird RMS検索アドオン用 native-wrapper 1.0.0 Installer"
            Manufacturer='Komari co. ltd.' InstallerVersion='200'
            Languages='1041' Compressed='yes' SummaryCodepage='932'
            Platform='x64'
        />
        <Media Id='1' Cabinet='nw.cab' EmbedCab='yes' DiskPrompt='1枚目' />
        <Property Id='DiskPrompt' Value="Thunderbird RMS検索アドオン用 native-wrapper 1.0.0 Installer [1]" />
        <Directory Id='TARGETDIR' Name='SourceDir'>
            <Directory Id='ProgramFiles64Folder' Name='PFiles'>
                <Directory Id='komari' Name='komari'>
                    <Directory Id='INSTALLDIR' Name='native-wrapper'>
                        <Component Id='MainExecutable' Guid='2C798864-55E0-4453-8698-7F442A76BFF9' Win64='yes'>
                            <File
                                Id='nativewrapperEXE'
                                Name='native-wrapper.exe' DiskId='1'
                                Source='native-wrapper.exe' KeyPath='yes'
                            />
                        </Component>
                        <Component Id='Manifest' Guid='CE0504F3-12FD-4E47-A46C-29D50C44351A' Win64='yes'>
                            <File
                                Id='manifestJSON'
                                Name='manifest.json' DiskId='1'
                                Source='manifest.json' KeyPath='yes'
                            />
                            <RegistryKey
                                Id='ManifestPath'
                                Root='HKLM'
                                Key='SOFTWARE\Mozilla\NativeMessagingHosts\jp.komari.native_wrapper'
                                ForceDeleteOnUninstall='yes'
                            >
                                <RegistryValue
                                    Type='string'
                                    Value='[INSTALLDIR]manifest.json'
                                />
                            </RegistryKey>
                        </Component>
                    </Directory>
                </Directory>
            </Directory>
        </Directory>
        <Feature Id='Complete' Level='1'>
            <ComponentRef Id='MainExecutable' />
            <ComponentRef Id='Manifest' />
        </Feature>
    </Product>
</Wix>

とりあえず、チュートリアルをそのままやってみて、そこからファイル名やレジストリとその値をちょいと調整するだけで簡単にmsi形式のインストーラを作成することができました。

実行ファイルを64bitで作成しちゃったので、Package要素にはPlatform='x64'、それぞれのComponent要素にはWin64='yes'を指定してあります。

このxmlファイルをWiXのcandleコマンドに指定して実行するとwixobjファイルが作成されるので、それを更にWiXのlightコマンドに指定して実行したらmsiファイルが作成されます。

前述のpackage.jsonでも、npm-scriptsのbuildに一連のコマンドが設定してあるので、npm run buildとするとソースコードからmsiファイルを作成できます。

配布とインストール

このmsiファイルを使用者に実行してもらうと、WindowsのProgram Files\komari\native-wrappernative-wrapper.exemanifest.jsonが配置され、そのパスがレジストリに登録されます。また、コントロールパネルなどからインストールアプリ一覧を見ると「Thunderbird RMS検索アドオン用 native-wrapper 1.0.0」が追加されているので、そこからアンインストール操作することで削除することができます。

ところで、このネイティブアプリはThunderbirdのアドオンから実行するためのものです。Thunderbirdにアドオンを追加するには、Thunderbirdのアドオンマネージャを手動で操作して前回作成したxpiファイルを指定する必要があります。

できれば今回作成したインストーラでアドオンの追加もされると嬉しいです。しかし、ウェブで調べてみると、以前はコマンドでアドオンを追加することができたが今は手動で追加するしかない、というような情報もあるので、アドオンとネイティブアプリは別々にインストールしてもらうことにしました。

レジストリ操作やなんやかんやすれば可能かもしれませんが、そこまでは調べていません。

次回はブラウザでメールアドレス検索を自動で実行する方法について

前回作成したアドオンと今回作成したネイティブアプリをインストールすることで、Thunderbirdのメール表示画面で「メールアドレスRMS検索」ボタンを押せばブラウザでRMSの検索画面が表示されるようになったと思います。

しかしまだ、メールアドレス欄にメールアドレスが入力されてはいるものの、検索条件の設定や検索の実行は手動でやらなければなりません。

次回の記事では、検索条件を設定して検索ボタンを押す動作も自動的に実行する方法を紹介したいと思います。