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

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

## ロールバック実行カスタムアクション

ロールバック実行カスタムアクションは、エラー発生時にエラーが発生するまでにインストーラーが行ったシステムの変更を元に戻すために利用されます。インストールで発生したバグは次のバージョンで修復することも考えられます。できる限りきれいな状態で次のインストーラーを適用するためにも、エラー発生時にきれいに後始末しておくことは重要です。カスタムアクションでシステムに変更を加えるなら、是非ともペアでロールバック実行カスタムアクションを実装しましょう。

ロールバック実行カスタムアクションを実装する上で、データの受け渡しの制限事項は遅延実行カスタムアクションと同じです。カスタムアクションに渡せるプロパティは1つだけで、カスタムアクションからプロパティで値を戻すことはできません。

ここでは例として、[前回、遅延実行カスタムアクションの説明](https://qiita.com/tohshima/items/6ce1043eaa3f898d4a14#%E9%81%85%E5%BB%B6%E5%AE%9F%E8%A1%8C%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)で使ったインストーラーに、ロールバックする機能を追加してみます。

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

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

extern "C" __declspec(dllexport) UINT delRunOnce(MSIHANDLE hInstall) {
    TCHAR inStr[MAX_PATH];
    DWORD inStrLen = MAX_PATH;
    HKEY hKey;
    // プロパティ経由で削除するアイテム名を受け取る
    MsiGetProperty(hInstall, "CustomActionData", inStr, &inStrLen);
    // レジストリ項目を削除
    RegOpenKeyEx(HKEY_LOCAL_MACHINE, KEY_RUNONCE, 0, KEY_ALL_ACCESS, &hKey);
    RegDeleteValue(hKey, inStr);
    RegCloseKey(hKey);
    return ERROR_SUCCESS;
}

extern "C" __declspec(dllexport) UINT setRunOnce(MSIHANDLE hInstall) {
    try {
        TCHAR inStr[MAX_PATH];
        DWORD inStrLen = MAX_PATH;
        HKEY hKey;
        // プロパティ経由で「アイテム名;フルパス」を受け取る
        if (MsiGetProperty(hInstall, "CustomActionData", inStr, &inStrLen) != ERROR_SUCCESS) {
            throw "Error in setRunOnce() at #1";
        }
        // 区切り文字;の位置を取得し
        std::string manipulateStr = inStr;
        std::string::size_type discripterPos = manipulateStr.find_first_of(";");
        if (discripterPos == std::string::npos) { // 区切り文字が見つからなければエラー
            throw "Error in setRunOnce() at #2";
        }
        // 「アイテム名」取り出し
        LPCSTR itemName = manipulateStr.substr(0, discripterPos).c_str();
        // 「フルパス」取り出し
        const BYTE* itemValue = reinterpret_cast<const BYTE*>(manipulateStr.substr(discripterPos + 1).c_str());
        // レジストリに書き込む
        if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, KEY_RUNONCE, 0, KEY_ALL_ACCESS, &hKey) != ERROR_SUCCESS) {
            throw "Error in setRunOnce() at #3";
        }
        if (RegSetValueEx(hKey, itemName, 0, REG_SZ, itemValue, strnlen(inStr, inStrLen)) != ERROR_SUCCESS) {
            throw "Error in setRunOnce() at #4";
        }
        if (RegCloseKey(hKey) != ERROR_SUCCESS) {
            throw "Error in setRunOnce() at #5";
        }
    }
    catch (LPCTSTR & msg) {
        PMSIHANDLE hRecord = MsiCreateRecord(1);
        MsiRecordSetString(hRecord, 0, msg);
        MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_ERROR | MB_OK), hRecord);
        return ERROR_INSTALL_FAILURE;
    }
    catch (...) {
        PMSIHANDLE hRecord = MsiCreateRecord(1);
        MsiRecordSetString(hRecord, 0, __FUNCTION__);
        MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_ERROR | MB_OK), hRecord);
        return ERROR_INSTALL_FAILURE;
    }
    return ERROR_SUCCESS;
}

extern "C" __declspec(dllexport) UINT retError(MSIHANDLE hInstall) {
    return ERROR_INSTALL_FAILURE;
}
```

`delRunOnce()`関数でRunOnceレジストリに登録した項目を削除します。この関数がエラーで終了すると、ロールバックすら失敗してしまいますので、あえてエラーチェックをしません。一方、`setRunOnce()`関数の方は、[前回、遅延実行カスタムアクションの説明](https://qiita.com/tohshima/items/6ce1043eaa3f898d4a14#%E9%81%85%E5%BB%B6%E5%AE%9F%E8%A1%8C%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)で使用したものと機能的には同じですが、前回よりエラーチェックを強化しています。catch文で使用した`MsiCreateRecord()`、`MsiRecordSetString()`、`MsiProcessMessage()`の各関数については次回説明します。`retError()`関数は、ロールバック実行を試すために使用する、ただエラーを返すだけの関数です。

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

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="7BB1445E-85E8-4968-B2C7-B3491CEB0A3A" Name="Part17_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="bfb0cc49-d094-4a96-b9f7-3f771addb2e0">
		<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"/>
        <!-- RunOnceレジストリに登録する -->
        <CustomAction Id="aSetRunOnceArg" Property="aSetRunOnceExe" Value="tohshima Products;&quot;[#fExe]&quot; &quot;This is test!&quot;" Execute="immediate"/>
        <CustomAction Id="aSetRunOnceExe" BinaryKey="CA" DllEntry="setRunOnce" Execute="deferred" Impersonate="no"/>
        <!-- 登録したRunOnceレジストリを削除する -->
        <Property     Id="aDelRunOnceExe" Value="tohshima Products"/>
        <CustomAction Id="aDelRunOnceExe" BinaryKey="CA" DllEntry="delRunOnce" Execute="rollback" Impersonate="no"/>
        <!-- エラーを起こしてロールバックさせる -->
<!--(1) <CustomAction Id="aRetError"      BinaryKey="CA" DllEntry="retError"   Execute="deferred"/>-->
        <InstallExecuteSequence>
            <Custom Action="aSetRunOnceArg" After="InstallInitialize">NOT Installed</Custom>
            <Custom Action="aSetRunOnceExe" After="aSetRunOnceArg">NOT Installed</Custom>
            <Custom Action="aDelRunOnceExe" After="aSetRunOnceExe">NOT Installed</Custom>
<!--(2)     <Custom Action="aRetError"      After="aDelRunOnceExe">NOT Installed</Custom>-->
        </InstallExecuteSequence>

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

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

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{7E7FE43C-6E59-428E-B9AC-20D8571EA236}">
                <File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes" />
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

BinaryエレメントからInstallExecuteSequenceエレメントまでに着目してください。レジストリ項目を追加する`aSetRunOnceArg`と`aSetRunOnceExe`のカスタムアクションは前回の記述と同様です。レジストリ項目を削除する`aDelRunOnceExe`カスタムアクションの入力として使用する`aDelRunOnceExe`プロパティは、インストール時に動的に変化する要素がない[^1]ため、Propertyエレメントで定義します。

このままインストーラーを実行すると、そのまま普通にインストールが終了します。ロールバックの実験のためには、Product.wxsの(1)と(2)のコメントアウトを外し、行を有効にします。これを実行すると、エラーが発生し、ロールバックカスタムアクション`aDelRunOnceExe`が呼ばれ、RunOnceに登録したレジストリ項目が削除されることが観測できます。

## コミット実行カスタムアクション

コミット実行カスタムアクションは、すべての遅延実行されるアクション（標準アクションとカスタムアクション）がエラーなく完了すると実行されます。データの受け渡しの制限事項は遅延実行カスタムアクションと同じです。カスタムアクションに渡せるプロパティは1つだけで、カスタムアクションからプロパティで値を戻すことはできません。

ここでは、例として即時実行カスタムアクションで[Part.15の下準備](https://qiita.com/tohshima/items/ff300295c85dffc0af18#%E4%B8%8B%E6%BA%96%E5%82%99)で作成した`SimpleMsgBox.exe`を、復帰を待たないオプションをつけて実行します。このとき、`SimpleMsgBox.exe`のダイアログボックスは表示されたままになりますが、このあとすぐダイアログボックスを閉じるカスタムアクションをコミット実行して、インストールを終了します。

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

extern "C" __declspec(dllexport) UINT closeMsgBox(MSIHANDLE hInstall){
    TCHAR inStr[MAX_PATH];
    DWORD inStrLen = MAX_PATH;
    // SimpleMsgBox.exeがダイアログボックスを表示するまで少し時間がかかるので、しばらく待つ
    Sleep(5000);
    // プロパティ経由で「ウィンドウクラス;ウィンドウタイトル」を受け取る
    MsiGetProperty(hInstall, "CustomActionData", inStr, &inStrLen);
    // 区切り文字;の位置を取得し
    std::string manipulateStr = inStr;
    std::string::size_type discripterPos = manipulateStr.find_first_of(";");
    // 「ウィンドウクラス」取り出し
    std::string wClass = manipulateStr.substr(0, discripterPos);
    // 「ウィンドウタイトル」取り出し
    std::string  wTitle = manipulateStr.substr(discripterPos + 1);
    // ダイアログボックスのウィンドウハンドル取得
    HWND nHwnd = FindWindow(wClass.c_str(), wTitle.c_str());
    if (nHwnd) {
        // ダイアログボックス上の[OK]ボタンのウィンドウハンドル取得
        HWND buttonHwnd = NULL;
        buttonHwnd = FindWindowEx(nHwnd, buttonHwnd, "Button", NULL);
        if (buttonHwnd) {
            // マウスのクリックをシミュレート
            SendMessage(buttonHwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
            SendMessage(buttonHwnd, WM_LBUTTONUP, MK_LBUTTON, 0);
        }
    }
    return ERROR_SUCCESS;
}
```

今回作成した`closeMsgBox()`関数は、「ウィンドウクラス;ウィンドウタイトル」の書式で閉じるべきダイアログボックスの情報を受け取り、ダイアログボックス上の[OK]ボタンにマウスクリックのイベントを送り、ダイアログボックスを閉じます。この実験では`SimpleMsgBox.exe`がダイアログボックスを表示するよりも早く`closeMsgBox()`関数を呼び出すカスタムアクションが実行されてしまうので、`closeMsgBox()`関数の先頭で５秒待たせています。

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

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="A95C5F45-FD77-48F0-AB9C-101FD9797BBB" Name="Part17_03" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="cbcdffad-f666-45c9-8ab8-6325b7582062">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <Binary Id="sMsgBox_exe" SourceFile="SimpleMsgBox.exe" />
        <Binary Id="CA" SourceFile="CA\CustomAction.dll" />
        <!-- SimpleMsgBox.exeを起動してダイアログボックスを表示し、終了を待たずに戻る -->
        <CustomAction Id="aShowMsg" BinaryKey="sMsgBox_exe" ExeCommand="&quot;This is test.&quot;" Return="asyncNoWait" Execute="immediate" />
        <!-- コミット実行でダイアログボックスを閉じる -->
        <Property Id="aCloseMsg" Value="#32770;SimpleMsgBox"/>
        <CustomAction Id="aCloseMsg" BinaryKey="CA" DllEntry="closeMsgBox" Execute="commit" />
        <!-- -->
        <InstallExecuteSequence>
            <Custom Action="aShowMsg" After="InstallInitialize">NOT Installed</Custom>
            <Custom Action="aCloseMsg" After="aShowMsg">NOT Installed</Custom>
        </InstallExecuteSequence>

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

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

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{E2A0CFB2-44C9-4364-B6A8-A09284AF62EB}">
                <File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

BinaryエレメントからInstallExecuteSequenceエレメントまでに着目してください。`aShowMsg`カスタムアクションは、`Return="asyncNoWait"`を指定することで、`SimpleMsgBox.exe`を呼び出すと終了を待たずに戻ってきます。`aCloseMsg`のカスタムアクションに与えるプロパティは、カスタムアクションと同名のプロパティ`aCloseMsg`に`Property`エレメントを使って格納します。そして、`aCloseMsg`カスタムアクションによって、`SimpleMsgBox.exe`が開いたダイアログボックスを閉じます。

[^1]: 一方で`aSetRunOnceExe`プロパティは、インストールするファイルへのフルパスを含んでおり、これはインストール実行時でなければ決まらないため、カスタムアクションで設定する必要があります。

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