Windows Installer手引書 Part.15 カスタムアクションで外部コマンドを実行する

[**前の記事へ**](https://qiita.com/tohshima/items/72d1e7602a48055c55f5)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/6ce1043eaa3f898d4a14)

## 下準備

今回は下準備として、コマンドラインに与えたメッセージをコマンドプロンプトとダイアログボックスに表示するサンプルプログラムを作ります。 これをインストーラーの外部コマンドとして実行することを考えます。

```c:SimpleMsgBox.c
#pragma comment(lib, "User32.lib")
#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[]){
    if(argc > 1){
        printf("Message : %s\n", argv[1]);
        MessageBox(0, argv[1], "SimpleMsgBox", MB_OK);
    }
    return 0;
}
```

これを、例えばVisual Studio 2019がインストールされていれば、スタートメニューから`x86 Native Tools Command Prompt for VS 2019`を起動して、下記コマンドでコンパイルし、`SimpleMsgBox.exe`を得ます。

```DOS
C:\>cl SimpleMsgBox.c
```

実行してみます。

```DOS
C:\>SimpleMsgBox "This is TEST."
Message : This is TEST.
```

これに加えて、以下のようなダイアログボックスが表示されればOKです。
![SimpleMsgBox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/0201c56a-41f9-99d4-5f7a-767906d593ab.png)

## 実行するプログラムをMSIのBinaryテーブルに格納する

インストール時に使用するプログラムをインストーラーに同梱する方法はいくつかありますが、まずMSI内に格納する方法を説明します。前準備で作成した`SimpleMsgBox.exe`をカスタムアクションで実行させてみます。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="F0B93737-A75E-4062-AD3B-5A8D2FD64CEB" Name="Part15_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="d66378d7-b106-43f3-851a-c715827569b4">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate />

        <Binary Id="msgBox_exe" SourceFile="SimpleMsgBox.exe" />
        <CustomAction Id="aShowMsg" BinaryKey="msgBox_exe" ExeCommand="&quot;This is test.&quot;" Return="check" Execute="immediate" />
        <InstallExecuteSequence>
            <Custom Action="aShowMsg" After="InstallInitialize"></Custom>
        </InstallExecuteSequence>

		<Feature Id="ProductFeature" Title="Part15_01" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="Part15_01" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{BA64B660-AD44-4F55-BEA3-5A57E6B73BAF}" >
                <File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

9行目の`Binary`エレメントでMSIに`SimpleMsgBox.exe`を格納し、10行目の`CustomAction`エレメントの`BinaryKey`アトリビュートにそのIDを指定します。`ExeCommand`アトリビュートはコマンドラインオプションを指定するのですが、`Binary`エレメントで格納したプログラムを実行する際には必ず必要で、コマンドラインオプションがなければ、`ExeCommand=""`のように空の文字列を指定します。今回の例ではスペース文字を含む文字列をコマンドラインオプションとして渡すため、文字列を`"`でくくる必要がありますが、XMLで表記する際は、`&quot;`でくくります。

実行すると、コマンドプロンプトとダイアログボックスが表示されるのが観測できます。`SimpleMsgBox.exe`はコンソールアプリなのでコマンドプロンプトが表示されますが、Windowsデスクトップアプリとして作成すれば表示されません。

## インストールしたファイルをカスタムアクションで実行する

コンポーネントでインストールしたファイルをカスタムアクションで実行できます。このアクションのためにファイルを配置するシーケンスの実行順が変わるわけではないので、アクションの実行タイミングに大きな制約があります。コンポーネントによるファイルの配置は、InstallFilesアクションによりdifferdのタイミングで実行されますので、これ以降に実行する必要があります。

ここでは例として、`SimpleMsgBox.exe`をインストールし、ファイルが配置されたらすぐに実行するようなインストーラーを作成します。`SimpleMsgBox.exe`をコンポーネントでインストールするように変更し、`CustomAction`エレメント、`InstallExecute`エレメントで実行できるようにします。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="13D7AE55-73BD-49FB-8080-109059450246" Name="Part15_03" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="845310dc-24c4-442c-895d-859f3c0b0936">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate />

        <CustomAction Id="aShowMsg" FileKey="fExe" ExeCommand="&quot;This is test.&quot;" Return="check" Execute="deferred"/>
        <InstallExecuteSequence>
            <Custom Action="aShowMsg" After="InstallFiles">NOT Installed</Custom>
        </InstallExecuteSequence>
            
		<Feature Id="ProductFeature" Title="Part15_03" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="Part15_03" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{4539C5B7-A2D8-4A58-9979-3906B6C706A3}" >
                <File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes" />
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

ここで気を付けなければならないのは、11行目のcustomエレメントのConditionに`NOT Installed`としているところです。これを忘れて常に実行するようにすると、アンインストールできないインストーラーができてしまいます。アンインストール時は、このカスタムアクションを実行するタイミングでは`SimpleMsgBox.exe`が消されたあとであるため、エラー終了するのです。

## コンソールアプリ実行時にコマンドプロンプトを表示しない

WiX Toolsetは、コンソールアプリをカスタムアクションで実行する際、コマンドプロンプトを表示させない機能を持っています。コンソールアプリを直接実行するのではなく、コンソールアプリを実行する特別なカスタムアクションを実行することで、これを実現します。WiX Toolsetの公式マニュアルでは、[こちらに説明があります](https://wixtoolset.org/documentation/manual/v3/customactions/qtexec.html)。上の節で作成したインストーラーのカスタムアクションをこの機能で実装し直してみます。

最初にWiX Toolsetのカスタムアクションを使えるようにするために、Visual Studioのソリューションエクスプローラで、Referencesに`WixUtilExtension.dll`（WiX Toolsetのインストール先にあります）を追加します。
![UtilExtension.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/681f0ccc-751b-8656-7735-aa34bb6ec568.png)
そして、下記の通りソースを変更します。変更点は、`CustomAction`エレメント、`InstallExecuteSequence`エレメントです。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="26ABF8AF-7EBC-4BAB-AA2A-3FAB2340B4A8" Name="Part15_02" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="1ef29435-8dad-4e82-8073-3bd966e99da4">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate />

        <CustomAction Id="setCmdLine" Property="aShowMsg" Value="&quot;[#fExe]&quot; &quot;This is test.&quot;" />
        <CustomAction Id="aShowMsg" BinaryKey="WixCA" DllEntry="WixQuietExec" Return="check" Execute="deferred" />
        <InstallExecuteSequence>
            <Custom Action="setCmdLine" After="InstallFiles">NOT Installed</Custom>
            <Custom Action="aShowMsg" After="setCmdLine">NOT Installed</Custom>
        </InstallExecuteSequence>

        <Feature Id="ProductFeature" Title="Part15_02" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="Part15_02" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{19400105-A137-4138-A192-D502DA9ABE3B}" >
                <File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes" />
            </Component>
        </ComponentGroup>
	</Fragment>
</Wix>
```

実行すると、ダイアログボックスだけが表示され、コマンドプロンプトが表示されないことがわかります。利用方法の詳細は[公式マニュアル](https://wixtoolset.org/documentation/manual/v3/customactions/qtexec.html)を読んでいただくとして、ここでは要点だけ説明します。

気を付けなければいけないのは、コマンドライン文字列を設定する`setCmdLine`アクションです。WiX Toolsetの`WixQuietExec`アクションを利用する場合、`immediate`で実行する場合は`WixQuietExecCmdLine`プロパティを、`deferred`で実行する場合は`WixQuietExec`アクションの`Id`と同名のプロパティを利用する必要があります。この例では、`deferred`で実行するため、`aShowMsg`プロパティにコマンドライン文字列を格納しています。

また、この例ではコンポーネントでインストールしたファイルをカスタムアクションで実行します。インストールするファイルのインストール先のフルパスは、インストール実行時にならないと決まりません。そのため、コマンドライン文字列も実行時に取得してプロパティに設定する必要があります。このような場合は、カスタムアクションを使ってプロパティにコマンドライン文字列を設定します。この例では、インストール後のファイルへのフルパスを`[#fExe]`のようにFileエレメントの`Id`を使って取得しています。こういった記法は、[こちらにまとめられています](https://docs.microsoft.com/en-us/windows/win32/msi/formatted)。

## 既存のプログラムを利用する

OSが最初から持っているコマンドや他の製品がインストールしたプログラムを、これまで説明した方法で実行する場合は、実行タイミングを気にせず利用できます。しかし、Windows Installerの仕組み上、コマンドが標準出力に吐き出した文字列を、MSIの中で直接利用することができません[^1]。このようなことがしたい場合は、DLL形式のカスタムアクションをC言語[^2]で作成する必要があります。

## 外部コマンドの戻り値の利用

外部コマンドの実行結果をエラーにしたいときは、[カスタムアクションの仕様に合わせて、エラーコードを戻す](https://docs.microsoft.com/en-us/windows/win32/msi/custom-action-return-values)ようにすれば処理されます。

`SimpleMsgBox.exe`がエラーメッセージを返すように変更してみます。エラーコードのマクロは`msi.h`に定義されているので、includeします。

```c:SimpleMsgBox.c
#pragma comment(lib, "User32.lib")
#include <windows.h>
#include <stdio.h>
#include <msi.h>

int main(int argc, char *argv[]){
    if(argc > 1){
        printf("Message : %s\n", argv[1]);
        MessageBox(0, argv[1], "SimpleMsgBox", MB_OK);
    }
    return ERROR_INSTALL_FAILURE;
}
```

これを使ってインストーラーを作成すると、インストーラーがエラー終了します。

## インストール後にカスタムアクションのConditionを変更する

「あぁ、ここをこういうConditionにしておけばアンインストールできたのに」と思っても、あとの祭り。でも大丈夫。ここでは、カスタムアクションのConditionの設定ミスでアンインストールできなくなった製品をアンインストールする方法を説明します[^3]。

ここでは、例として「インストールしたファイルをカスタムアクションで実行する」で使用したMSI(Part15_03)を改造して、アンインストールできないインストーラーを作成します。

```xml
<Custom Action="aShowMsg" After="InstallFiles">NOT Installed</Custom>
```

となっているところを

```xml
<Custom Action="aShowMsg" After="InstallFiles"></Custom>
```

と変更して、エラーによりアンインストールできないことを確認します。アンインストールすると、下図のようなエラーメッセージが表示され、ロールバックされるはずです。
![Part15_03_InstallError.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/a3fda99b-9062-db9d-08fd-c5332a683414.png)

#### アンインストールで使用されるMSIの場所を探す

インストールを実行すると、Windows Installerは使用したMSIファイルを特別な場所に特別なファイル名でキャッシュします[^4]。Windowsの設定やコントロールパネルからアンインストールを実行すると、このキャッシュされたMSIファイルが使用されます。キャッシュされた自分のMSIの場所は、下記のようにレジストリに書かれています。

> キー：HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\55EA7D31DB37BF940808010995542064\InstallProperties
> 名前：LocalPackage
> 値：C:\Windows\Installer\3661f6f5.msi

キー名の途中にある`S-1-5-18`はインストールを実行したユーザーを示していて、管理者権限のついた第1ユーザーでインストールを実行すると、必ずこの名前になります。また、`55EA7D31DB37BF940808010995542064`は、packed GUIDという形式でコード化されたインストーラーの製品コードです。コード化と言っても、下図のようにGUIDの各文字を並べ替えただけのものです。図の上がGUID、下がPackd GUIDです（[変換用のスクリプトがここで紹介されて](https://www.symantec.com/connect/blogs/guid-converter)います）。MSIのファイル名`3661f6f5.msi`はランダムに命名されます。実験すると異なるファイル名が格納されていることでしょう。
![PackedGUID変換.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/852186db-ef0a-737e-a2c0-ee5c14d7d17e.png)

#### OrcaでカスタムアクションのConditionを書き換える

レジストリに書かれている`C:\Windows\Installer`のフォルダは隠しフォルダであり、また、書き換えには管理者権限が必要です。Orcaを直接管理者として実行し、「ファイルを開く」ダイアログボックスで、ファイル名欄にフルパスでファイル名を指定すれば、開くのは簡単です。
![Part15_OpenMsi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/25b66c06-931a-b6c9-f932-3a1ff44e52d2.png)
開いたら、`InstallExecuteSequence`の`aShowMsg`アクションのConditionを確認します。空欄になっているはずなので`NOT Installed`と入力して保存します。
![Part15_EditORCA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/d10a1d23-664d-16d0-52a1-fe859853b0d3.png)
これでアンインストール可能になりました。

[^1]: せめて標準出力の内容をプロパティに保存してくれれば、便利なのですが。こういうのは、自分で作るしかありません。
[^2]: しつこいですが、VBScriptやJScriptなどのスクリプトで実装したカスタムアクションを、試作以外で使うべきではありません。C言語がなじめないなら、C#などのマネージドな言語でカスタムアクションを実装する選択肢もあるようです。ランタイムのケアが面倒そうですが。
[^3]: 恥ずかしながら、この記事を書きながら実験していたところ、自分でもやらかしてしまいました。
[^4]: このMSIファイルは、一部の情報を削除しているようですので、利用には注意が必要です。

[**前の記事へ**](https://qiita.com/tohshima/items/72d1e7602a48055c55f5)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/6ce1043eaa3f898d4a14)