EternalWindows
MSI カスタムアクション編 / カスタムアクション(DLL)

カスタムアクションをEXEかDLL、どちらに実装するかの1つの目安として、 インストールを中断させる必要があるかどうかが関わってきます。 たとえば、特定のアプリケーションがインストールされていない場合に インストールを続行したくないとすれば、 そのインストールを確認するコードはDLLに実装するべきです。 これは、DLLがmsiexecのアドレス空間で動作できることから、 関数の戻り値を通じてシーケンスの実行を止めることができるからです。 標準アクションの中には、このようなインストール前に 確認を行うLanuchCoditionsやAppSearchというアクションが存在するため、 まずはこれらのアクションで目的の動作を達成できないかは検討しておくべきといえます。

カスタムアクションでインストールの実行の可否を決定するには、 当然ながらそのアクションをインストールに関わるアクションの前に実行しておくことになります。 セットアッププロジェクトのカスタム動作で作成したカスタムアクションは、 インストールを終えた後に実行されるように設定されているため、 まずはこれをインストールの前に実行されるように設定する必要があります。 そして、もう1つ重要なのが、カスタムアクションを実装するDLLファイルをバイナリとしてMSIファイルに含めることです。 ファイルシステムエディタでファイルを追加したとき、 デフォルトではそのファイルはMSIファイルの中にあるキャビネットファイルに含まれることになり、 これはファイルのインストール段階で展開されますから、 これではインストールの前にDLLを参照することはできません。

上記の作業をセットアッププロジェクトで行ってみましょう。 まずは、ファイルシステムエディタでDLLファイルを追加します。 このDLLは最終的にバイナリとしてMSIファイルに含まれ、 何処かのフォルダに配置されるということはありませんから、対象とするフォルダは問いません。 追加が終了すれば、そのファイルの情報をプロパティウインドウで表示します。

ExcludeをTRUEに設定します。 これにより、DLLはMSIファイルにバイナリとして含まれます。 より具体的にいえば、Binaryテーブルにその旨が書き込まれます。 次に、このDLLをカスタムアクションとして利用できるよう、 カスタム動作エディタで追加します。

ここでは、「インストール」を対象としていますが、これに関しても種類は問いません。 最終的にはシーケンスの値を変更し、後述するカスタムアクションタイプも変更するからです。 DLLのプロパティウインドウは、次のようになっています。

EntryPointに設定した関数をMSIが呼び出すことになります。 ここでは、Installという関数になっていますが、 これは「インストール」の下にDLLを追加したからであって、 関数名は自由に変更することができます。 ここに指定した関数が、DLLでエクスポートされていない場合は、 ビルドに失敗することになります。 以上の作業を確認したら、ビルドをしてMSIファイルを作成してください。 そして、Orcaを開いて修正作業を行います。

上図は、InstallExecuteSequenceを開いて、DLLのカスタムアクションのレコードを選択しています。 何故、このレコードが標準アクションではなくカスタムアクションを示すかが特定できたのかについては、 Actionカラムの値がCustomActionテーブルに含まれているということ、 そのカスタムアクションが今回追加したDLLによるものだと特定できるのは、 CustomActionテーブルのTypeカラムやTargetカラムを確認したからです。 このDLLのカスタムアクションはインストール前に実行されなければならないわけですが、 具体的にはどのような値が理想なのでしょうか。 たとえば、起動条件の判定を行うLanuchCoditionsのSequenceカラムは400となっていますから、 その前か後くらいが妥当なところであると思われます。 ここでは、LanuchCoditionsの前に実行するものとし、399を指定します。 標準アクションは、Sequenceカラムの値を10の倍数にする傾向があるため、 カスタムアクションではなるべく10の倍数に設定しないようにするべきとされています。

続いて、カスタムアクションタイプの設定作業に入ります。 先の図で選択したレコードのActionカラムの値を持つレコードをCustomActionテーブルで探します。

カスタムアクションタイプは、そのアクションが何を基にして成り立っているのかを示します。 たとえば、DLLのカスタムアクションならば、DLLを基にしていますから、 カスタムアクションタイプのTypeにはDLLを表す数値が格納されることになります。

Type 説明 Source
1 DLLがバイナリとして含まれていることを示す。 Binary テーブル
17 DLLが製品と共にインストールされることを示す。 File テーブル
2 EXEがバイナリとして含まれていることを示す。 Binary テーブル
18 EXEが製品と共にインストールされることを示す。 File テーブル
51 プロパティに値を設定することを示す。 Property テーブル

EXEやDLLをMSIファイルに含めるときには、ファイルとバイナリの2種類がありますから、 カスタムアクションタイプも2種類存在することになります。 Sourceは、その基になるEXEやDLLの情報が格納されているテーブルへの外部キーであり、 DLLをバイナリとして含めたならば、SourceはBinaryテーブルとなります。 バイナリのDLLはTypeの値を1とすべきですから、上図ではTypeに1を設定しています。 恐らく、最初は1025という値が設定されていたと思われますが、 これは正確には1 + 1024であり、1024(リファレンス表記でmsidbCustomActionTypeInScript)という値はあくまでオプションです。 このオプションが含まれてはならないため、明示的にそれを省くようにしたのですが、 この理由については後述します。

以上の作業が終了すれば、MSIファイルを起動する準備は整ったといえます。 追加したアクションはインストールが行われる前に実行され、 DLLの関数も呼ばれることになるはずです。 ここからは、このDLLの関数の実装について見ていきます。 MSIはプロパティウインドウのEntryPointで指定した関数が、 次のようなプロトタイプを持つことを想定しています。

__stdcall CustomAction(MSIHANDLE hInstall)

関数名のCustomActionは自由に変更することができます。 hInstallは、MSIの機能を利用するためのインストールハンドルです。 たとえば、MSIはインストールのためにMSIデータベースをオープンするわけですが、 そのデータベースのハンドルをDLLが必要とする場合は、 インストールハンドル指定してMsiGetActiveDatabaseを呼び出すことができます。 つまり、既に開かれているデータベースのハンドルを利用するということであるため、 DLLが明示的にデータベースをオープンする必要はなくなります。 関数の戻り値の型については特に定義されていませんが、 次の定数のいずれかを返すべきとされています。 ただし、オプションとしてmsidbCustomActionTypeContinue(64)が含まれている場合は、 戻り値の値は無視されます。

戻り値 説明
ERROR_FUNCTION_NOT_CALLED アクションが実行できなかったことを示す
ERROR_SUCCESS アクションが成功したことを示す。
ERROR_INSTALL_USEREXIT ユーザーが明示的に終了させたことを示す。 この戻り値は、エラーとして解釈される。
ERROR_INSTALL_FAILURE 修復不可能なエラーが生じたことを示す。 この戻り値は、エラーとして解釈される。
ERROR_NO_MORE_ITEMS 後続のアクションをスキップすることを示す。

今回は、DLLが定義する特定の条件が満たされているかを調べ、 その結果がFALSEである場合はインストールを中断することになりますから、 そのようなときにはERROR_INSTALL_USEREXITやERROR_INSTALL_FAILUREを指定することになるでしょう。 結果がTRUEである場合はインストールを続けるため、ERROR_SUCCESSを指定します。

それでは、実際にDLLの関数にコードを書いてみましょう。 まず、どのような条件を判定するかを決めなければなりませんが、 具体的な候補としては何が挙げられるでしょうか。 たとえば、インストールの際に自社サーバーにアクセスして何らかのデータを確認したい場合は、 ネットワークへの接続の有無を確認する必要があるでしょう。 また、MSIによるインストールではファイルやレジストリキーが既に存在する場合は無条件に上書きするため、 上書きの対象となるコンポーネントを事前に列挙しておくのもよいかもしれません。 どのような条件を判定するにしても、恐らく最終的には次のようなコードを記述することになると思われます。

extern "C" __declspec(dllexport) __stdcall Install(MSIHANDLE hInstall)
{
	// 条件を調べる

	if (!bInstall) { // 結果はFALSEであった
		MessageBox(NULL, TEXT("Error Message"), NULL, MB_ICONWARNING);
		return ERROR_INSTALL_USEREXIT;
	}

	return ERROR_SUCCESS;
}

この関数はMSIが呼び出せるようにエクスポートしていなければならないため、 dllexportキーワードを利用することにしています。 もちろん、関数のエクスポートはdefファイルを利用しても構いません。 関数内部では特定の条件を判定し、その結果がFALSEである場合は、 MessageBoxを通じてインストールを続行できない旨を伝えようとしています。 これは一見すると問題ないように思えますが、 インストールにはUIレベルという概念があったことを忘れてはいけません。 MsiSetInternalUIでINSTALLUILEVEL_NONEを指定したインストールはサイレントインストールであり、 このときにはユーザーインターフェースを表示するわけにはいきませんから、 事前にUIレベルを確認する必要があるのです。

UIレベルは、MSIによってUILevelプロパティに設定されることになっています。 設定される値は2〜5であり、2が最もUIレベルの低いINSTALLUILEVEL_NONEを表しています。 このため、UILevelプロパティで取得した値が2よりも大きければ、 ユーザーインターフェースを表示しても問題がないことになります。 プロパティの取得はMsiGetPropertyで行うことができます。

UINT MsiGetProperty(
  MSIHANDLE hInstall,
  LPCTSTR szName,
  LPTSTR szValueBuf,
  DWORD *pchValueBuf
);

hInstallは、インストールハンドルを指定します。 szNameは、取得したいプロパティの名前を指定します。 szValueBufは、プロパティの値を受け取るためのバッファを指定します。 pchValueBufは、バッファのサイズを受け取る変数のアドレスを指定します。 次に、MsiGetPropertyの呼び出しの例を示します。

dwSize = sizeof(szBuf);
MsiGetProperty(hInstall, TEXT("UILevel"), szBuf, &dwSize);

UILevelプロパティの値を取得するために、 MsiGetPropertyの第2引数にはUILevelというプロパティ名を指定します。 これにより、szBufには2〜5のいずれかの値が格納されているはずです。 後はszBufの中身を数値に変換し、それが2より大きいかを比較することになりますが、 実は今回のように最初からプロパティの値を条件式として利用する場合は、 MsiEvaluateConditionを呼び出したほうが効率的です。

MSICONDITION MsiEvaluateCondition(
  MSIHANDLE hInstall,
  LPCTSTR szCondition
);

hInstallは、インストールハンドルを指定します。 szConditionは、評価したい条件式を指定します。 この条件式のフォーマットは、シーケンステーブルのConditionカラムと同一です。 戻り値がMSICONDITION_TRUEであるときは条件式がTRUEであり、 MSICONDITION_FALSEである場合は条件式がFALSEと評価されたことを意味します。 MSICONDITION_ERRORのときは、条件式のフォーマットが不正である可能性があります。 次に、MsiEvaluateConditionを利用した例を示します。

extern "C" __declspec(dllexport) __stdcall Install(MSIHANDLE hInstall)
{
	if (!bInstall) {
		if (MsiEvaluateCondition(hInstall, TEXT("UILevel > 2")) == MSICONDITION_TRUE)
			MessageBox(NULL, TEXT("Error Message"), NULL, MB_ICONWARNING);
		return ERROR_INSTALL_USEREXIT;
	}

	return ERROR_SUCCESS;
}

MsiEvaluateConditionの第2引数には、UILevelプロパティが2より大きいかを表す条件式を指定しています。 このとき、関数の戻り値がMSICONDITION_TRUEであるということは、 サイレントインストールではないということですから、 MessageBoxを表示しても問題ありません。 MSIにはメッセージを表示することのできる関数として、 MsiProcessMessageがありますが、この関数は内部でUIレベルを考慮しているため、 サイレントインストールのときにはダイアログが表示されることはありません。

In-Script Execution Optionsについて

セットアッププロジェクトのカスタム動作エディタでカスタムアクションを追加した場合、 そのカスタムアクションのタイプには必ずIn-Script Execution Optionsと呼ばれる オプションフラグが追加されます。 カスタム動作エディタのインストールが終了した後に、 確定やロールバックのカスタムアクションが実行されるのは、 このオプションの働きによるものです。

オプションの値 動作 MsiGetMode
1024 インストールかアンインストール MSIRUNMODE_SCHEDULED
1536 確定 MSIRUNMODE_COMMIT
1280 ロールバック MSIRUNMODE_ROLLBACK

オプションが含まれているカスタムアクションにてMsiGetModeを呼び出した場合、 現在どのオプションでコードを実行しているかを調べることができます。 このため、インストール、確定、ロールバックの全てのエントリポイントを1つの関数に設定し、 その中でMsiGetModeを呼び出して条件式を別ければ、 必要な動作が3つでも、実装する関数は1つで済むことになります。 逆に、このような確定やロールバックというメカニズムが必要でない場合は、 今回のようにオプションを取り除いても構いません。 そもそも、このメカニズムはファイルをインストールするための標準アクション等が実行されて からでないと有効にはならないため、今回のようにインストールの前に実行する カスタムアクションでは必ず取り除いておく必要があります。

In-Script Execution Optionsは、スクリプトファイルにプロパティを渡す手段でもあります。 プロパティウインドウのCustomActionDataには、スクリプトファイルに渡したいプロパティの組み合わせを指定し、 たとえば、それがTARGETDIRとUILevelなのであれば、[TARGETDIR],[UILevel]のように指定します。 これにより、オプションを含んでいるカスタムアクションに関しては、 CustomActionDataで指定したプロパティを参照できるようになります。 残念なことに、カスタム動作エディタではオプションが無条件に追加されますから、 既定のままではDLLからMSIが設定したプロパティの値を直接取得することはできません。 つまり、TARGETDIRの値が取得したくても、MsiGetPropertyにTARGETDIRを指定することはできず、 代わりにMsiGetPropertyにCustomActionDataという文字列を指定して、 TARGETDIRの値を取得することになるのです。 このためには、プロパティウインドウのCustomActionDataで[TAGETDIR]を指定するという作業が伴います。

基本的にIn-Script Execution Optionsは、よい影響をもたらさないと考えてよいと思われます。 特に有効となるプロパティが、CustomActionDataと一部のプロパティ(ProductCodeとUserSID)だけで あるというのは致命的です。 必要なプロパティが生じ度にプロパティウインドウのCustomActionDataで プロパティを指定しなければなりませんし、複数のプロパティの組み合わせから 目的のプロパティを抜き出す作業も必要です。 また、プロパティを条件式として利用できるMsiEvaluateConditionを 呼び出せなくなるというのも柔軟性を大きく欠くといえるでしょう。 オプションを取り除けば、MsiGetPropertyで目的のプロパティを直接指定できますし、 MsiEvaluateConditionを呼び出すこともできるようになります。



戻る