ロールバック実行カスタムアクション
ロールバック実行カスタムアクションは、エラー発生時にエラーが発生するまでにインストーラーが行ったシステムの変更を元に戻すために利用されます。インストールで発生したバグは次のバージョンで修復することも考えられます。できる限りきれいな状態で次のインストーラーを適用するためにも、エラー発生時にきれいに後始末しておくことは重要です。カスタムアクションでシステムに変更を加えるなら、是非ともペアでロールバック実行カスタムアクションを実装しましょう。
ロールバック実行カスタムアクションを実装する上で、データの受け渡しの制限事項は遅延実行カスタムアクションと同じです。カスタムアクションに渡せるプロパティは1つだけで、カスタムアクションからプロパティで値を戻すことはできません。
ここでは例として、前回、遅延実行カスタムアクションの説明で使ったインストーラーに、ロールバックする機能を追加してみます。
#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()
関数の方は、前回、遅延実行カスタムアクションの説明で使用したものと機能的には同じですが、前回よりエラーチェックを強化しています。catch文で使用したMsiCreateRecord()
、MsiRecordSetString()
、MsiProcessMessage()
の各関数については次回説明します。retError()
関数は、ロールバック実行を試すために使用する、ただエラーを返すだけの関数です。
以下に、このカスタムアクションの使用例を示します。
<?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;"[#fExe]" "This is test!"" 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の下準備で作成したSimpleMsgBox.exe
を、復帰を待たないオプションをつけて実行します。このとき、SimpleMsgBox.exe
のダイアログボックスは表示されたままになりますが、このあとすぐダイアログボックスを閉じるカスタムアクションをコミット実行して、インストールを終了します。
#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()
関数の先頭で5秒待たせています。
以下に、このカスタムアクションの使用例を示します。
<?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=""This is test."" 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
が開いたダイアログボックスを閉じます。
-
一方で
aSetRunOnceExe
プロパティは、インストールするファイルへのフルパスを含んでおり、これはインストール実行時でなければ決まらないため、カスタムアクションで設定する必要があります。 ↩