Windows Installer手引書 Part.16 C言語によるカスタムアクション１

[**前の記事へ**](https://qiita.com/tohshima/items/ff300295c85dffc0af18)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/626b0462be9ea010621e)

## 即時実行カスタムアクション

即時実行に指定したカスタムアクションは、インストール先環境に変更を加えない処理のために利用します。カスタムアクションを即時実行する場合は、後で説明する遅延実行する場合に比べて、比較的に自由に情報の入出力が行えるようになっています。カスタムアクションをC言語で記述したとき、C言語の関数に情報を入力する場合も、処理結果を出力する場合も、プロパティを使用します。

ここでは例として、特定のファイルへのフルパスを与えると、ファイルのディレクトリ部分と、ファイル名部分に分割するアクションを作成します。ここで入力のプロパティは`_inputPath`、出力のプロパティは、`_DirName`と`_BaseName`になります。

```C++:CustomAction.cpp
#pragma comment(lib, "msi.lib")
#include <windows.h>
#include <msiquery.h>
#include <string>

extern "C" __declspec(dllexport) UINT expandPath(MSIHANDLE hInstall){
    TCHAR inStr[MAX_PATH];
    DWORD inStrLen = MAX_PATH;
    // プロパティ経由でフルパスを受け取る
    MsiGetProperty(hInstall, "_inputPath", inStr, &inStrLen);
    // ファイル名直前の\の位置を取得し
    std::string manipulateStr = inStr;
    std::string::size_type discripterPos = manipulateStr.find_last_of("\\");
    // プロパティ経由で結果を返す
    MsiSetProperty(hInstall, "_DirName", manipulateStr.substr(0, discripterPos).c_str());
    MsiSetProperty(hInstall, "_BaseName", manipulateStr.substr(discripterPos + 1).c_str());
    return ERROR_SUCCESS;
}
```

ビルドするには、下記のコマンドをx86 Native Tools Command PromptやVisual Studioのメイクファイルから実行します。この結果、`CustomAction.dll`を得ることができます。

```DOS
cl /LD CustomAction.cpp
```

ここから、C言語のソースの詳細について説明していきます。カスタムアクションの関数プロトタイプは本例の様に記述しておけば、インストーラーから呼ぶことができます。この書き方なら、関数をDLL外部から参照するための`.def`ファイルが不要になるため、ビルドが簡単になります。

```C++:カスタムアクションのプロトタイプ宣言
extern "C" __declspec(dllexport) UINT 関数名(MSIHANDLE hInstall)
```

また、カスタムアクションのソースにあるように、`#pragma`を使ってリンクするライブラリを指定すれば、ビルドコマンドを変更する必要もなくなり、メンテナンス対象はC言語のソースだけになり、便利です。

関数の入出力に使われるプロパティのアクセスは、読み込みは`MsiGetProperty()`関数を、書き出しは`MsiSetProperty()`関数を使います。ここで使用するプロパティの名称には、特に制限がなく、[プロパティの一般的な命名規則](https://docs.microsoft.com/en-us/windows/win32/msi/restrictions-on-property-names)にのっとっていれば、どのような名前でも問題ありません。

カスタムアクションの関数は、[Custom Action Return Values](https://docs.microsoft.com/en-us/windows/win32/msi/custom-action-return-values)で説明されている以下の値を返すことができます。

|戻り値|説明|
|:----|:---|
|ERROR_FUNCTION_NOT_CALLED|アクションは実行されなかった。|
|ERROR_SUCCESS|アクションは成功した。|
|ERROR_INSTALL_USEREXIT|ユーザーが中断した。|
|ERROR_INSTALL_FAILURE|修復できないエラーが発生した。|
|ERROR_NO_MORE_ITEMS|エラーではないが、何らかの処理がスキップされた。|

気を付けなければならないのは、カスタムアクションがエラーを返すと、ロールバックが実行され元に戻される、ということです。特にアンインストール時にエラーが発生すると、製品がアンインストールできなくなってしまいます。アンインストール時のカスタムアクションの実行時エラーには十分気を付ける必要[^1]があります。

以下に、このカスタムアクションの使用例を示します。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="28123AA8-D6A7-424D-8495-7F5AE87C05AB" Name="Part16_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="a5a0b62e-b912-4d21-ab16-1f23c79a5ab3">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <Binary Id="CA" SourceFile="CA\CustomAction.dll"/>
        <Property Id="_inputPath" Value="C:\Windows\System32\calc.exe" />
        <CustomAction Id="aExpandPath" BinaryKey="CA" DllEntry="expandPath" Execute="immediate" Return="check"/>
        <CustomAction Id="aPrExpanded" Script="vbscript" Execute="immediate" Return="ignore">
            <![CDATA[
MsgBox("_DirName : "+Session.Property("_DirName")+Chr(13)+"_BaseName : "+Session.Property("_BaseName"))
			]]>
        </CustomAction>
        <InstallExecuteSequence>
            <Custom Action="aExpandPath" After="InstallInitialize">not Installed</Custom>
            <Custom Action="aPrExpanded" After="aExpandPath">not Installed</Custom>
        </InstallExecuteSequence>

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

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

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{46BEB206-817D-43DD-B86D-49A1C2406D4F}">
                <File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

`Binary`エレメントから`InstallExecuteSequence`エレメントまでに着目してください。以下のような処理を実行しています。

- `Property`エレメントで`_inputPath`プロパティに電卓アプリへのフルパスを格納
- さきに作成した`expandPath`カスタムアクションを実行（`aExpandPath`アクション）
- これにより、`_DirName`プロパティに電卓アプリが存在するパスが、`_BaseName`プロパティに電卓アプリのファイル名が格納される
- `_DirName`プロパティと`_BaseName`プロパティの内容をダイアログボックスに表示（`aPrExpanded`アクション）

## 遅延実行カスタムアクション

インストール先環境に変更を加えるカスタムアクションは、遅延実行する必要があります。遅延実行は、別のプロセスが起動され、そのうえで実行されるため、情報の受け渡しに大きな制限があります。カスタムアクションの入力として利用できるプロパティは、１つのカスタムアクションあたり１つのプロパティだけ。プロパティに値を出力することはできません。

ここでは例として、次回OS起動時に１回だけ実行される`RunOnce`レジストリにプログラムを登録するアクションを作成します。

```C++:CustomAction.cpp
#pragma comment(lib, "msi.lib")
#pragma comment(lib, "Advapi32.lib")
#include <windows.h>
#include <msiquery.h>

#define KEY_RUNONCE "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce"
#define STR_MAX 512

extern "C" __declspec(dllexport) UINT setRunOnce(MSIHANDLE hInstall){
    TCHAR inStr[MAX_PATH];
    DWORD inStrLen = MAX_PATH;
    HKEY hKey;
    // プロパティ経由でフルパスを受け取る
    MsiGetProperty(hInstall, "CustomActionData", inStr, &inStrLen);
    // レジストリに書き込む
    if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, KEY_RUNONCE, 0, KEY_ALL_ACCESS, &hKey) != ERROR_SUCCESS)
        return ERROR_INSTALL_FAILURE;
    if (RegSetValueEx(hKey, "tohshima Products", 0, REG_SZ, reinterpret_cast<const BYTE*>(inStr), strnlen(inStr, inStrLen)) != ERROR_SUCCESS)
        return ERROR_INSTALL_FAILURE;
    if (RegCloseKey(hKey) != ERROR_SUCCESS)
        return ERROR_INSTALL_FAILURE;
    return ERROR_SUCCESS;
}
```

`MsiGetProperty()`関数に注目してください。遅延実行時には、カスタムアクションに渡されるプロパティは、常に`CustomActionData`というプロパティ名に変換され渡されます。ここで、「変換」と書いたのは、プロパティに値を格納するときは、別のプロパティ名だからです。

以下に、このカスタムアクションの使用例を示します。このインストーラーでインストールしている`SimpleMsgBox.exe`は、[Part.15](https://qiita.com/tohshima/items/ff300295c85dffc0af18)の下準備で作成したプログラムです。このプログラムを、RunOnceレジストリに登録します。インストール後にOSを再起動すると、「This is test!」というメッセージが表示されます。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="1097FE92-F1AD-4910-BC6E-E3A8CF9E667D" Name="Part16_02" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="79571ef1-b65b-4c7f-9227-030216080610">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <Binary Id="CA" SourceFile="CA\CustomAction.dll"/>
        <CustomAction Id="aSetRunOnceArg" Property="aSetRunOnceExe" Value="&quot;[#fExe]&quot; &quot;This is test!&quot;" Execute="immediate"/>
        <CustomAction Id="aSetRunOnceExe" BinaryKey="CA" DllEntry="setRunOnce" Execute="deferred" Impersonate="no"/>
        <InstallExecuteSequence>
            <Custom Action="aSetRunOnceArg" After="InstallInitialize">NOT Installed</Custom>
			<Custom Action="aSetRunOnceExe" After="aSetRunOnceArg">NOT Installed</Custom>
        </InstallExecuteSequence>
            
		<Feature Id="ProductFeature" Title="Part16_02" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

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

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{2C4BA3D4-6F7A-4523-A232-5858713CBE8D}">
                <File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes" />
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

BinaryエレメントからInstallExecuteSequenceエレメントまでに着目してください。カスタムアクションに渡すデータをプロパティに格納しているのは、`aSetRunOnceArg`というIdのカスタムアクションですが、ここでは`aSetRunOnceExe`という名称のプロパティに値を格納しています。この名前は、C言語で書かれたカスタムアクションのIdと同じです。遅延実行するカスタムアクションでは、そのIdと同名のプロパティに値を設定すれば、値を渡すことができるのです。

以上見てきたように、即時実行されるカスタムアクションと遅延実行されるカスタムアクションは、引数の受け渡し方法が異なります。自作したカスタムアクションは、どちらで使用することを想定しているのか、しっかり管理しておく必要があります。

[^1]: アンインストール時はカスタムアクションはエラーを返さない、などの配慮が必要になってきます。テスト環境で発生しないエラーも、ほかの環境では起きるかもしれません。カスタムアクションが利用する機能について、十分なエラー処理を検討してください。

[**前の記事へ**](https://qiita.com/tohshima/items/ff300295c85dffc0af18)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/626b0462be9ea010621e)