Windows Installer手引書 Part.23 インストール物が使用中だったらどうなる

[**前の記事へ**](https://qiita.com/tohshima/items/9758f947e9567ad6ffa9)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/71102ac002a45b819eeb)

## 使用中のファイル

アンインストールとアップグレードインストールのとき、操作対象のファイルを開いていると削除や置き換えができないことがあります。ユーザーの不注意でファイルを開いたまま、というケースだけでなく、システムに常駐するアプリなどはインストーラーが適切にファイルを閉じないと、ファイルの使用中にインストーラーがファイルを削除することになります。インストーラーは、このような状況でも正しくインストールを進めるために、ユーザーにファイルが使われていることを知らせ、ユーザーがファイルを閉じることを促したり、ファイルを閉じずにアンインストールやアップグレードを進め、PCを再起動することでファイルを差し替える、等の選択肢を示すことができます。この目的のために、[FilesInUseダイアログ](https://docs.microsoft.com/en-us/windows/win32/msi/filesinuse-dialog)もしくは[RMFilesInUseダイアログ](https://docs.microsoft.com/en-us/windows/win32/msi/msirmfilesinuse-dialog)が用意されています[^1]。ただ、前者は互換性のために残されている、と考えてよいので、RMFilesInUseダイアログが使われると覚えておけば良いでしょう。[^2]

## 下ごしらえ

今回の解説の準備として、インストールしたデータファイルを開いたまま待機するプログラムを作ります。引数に指定したテキストファイルの先頭部分をダイアログに表示して、テキストファイルを閉じずに待機します。[OK]ボタンを押してダイアログを閉じれば、テキストファイルも閉じられます。引数にファイルを指定しなかった場合は、使用方法をダイアログに表示し待機します。

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

int main(int argc, char *argv[]){
    const size_t inStrSize = 1024;
    int retCode = 1;    // init as fail.
    char* outputMsg = NULL;
    char* usageMsg = "usage : \nkeepFileOpen [input file name]";
    char inStr[inStrSize];
    HANDLE inFileHandle = NULL;
    DWORD numberOfBytesRead;

    if(argc == 2){
        // ↓dwShareMode=0として、別のプロセスからのアクセスを拒否する
        inFileHandle = CreateFile(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        SetFilePointer(inFileHandle, 0, 0, FILE_BEGIN);
        ReadFile(inFileHandle, inStr, inStrSize, &numberOfBytesRead, NULL);
        outputMsg = inStr;
        retCode = 0;    // succsess
    }else{
        outputMsg = usageMsg;
    }
    MessageBox(0, outputMsg, "keepFileOpen", MB_OK);
    if(inFileHandle){
        CloseHandle(inFileHandle);
    }
    return retCode;
}
```

これをVisual Studioの「x86 Native Tools Command Prompt」等から下記のコマンドでビルドし、`keepFileOpen.exe`を得ます。

```
cl keepFileOpen.cpp
```

## インストールとアプリの起動

インストーラーが使用中のアプリを処理するにあたり、どの権限で起動したアプリがファイルを開いているのか、によってできることが変わってきます。

#### perMachineなインストーラーで、管理者権限でインストール物を起動したままアンインストール

下記の属性の組み合わせを使用します。

|エレメント|属性|値|意味|
|:--------|:---|:--|:--|
|Package|InstallScope|perMachine|管理者権限でインストール|
|CustomAction|Impersonate|no|管理者権限でインストール物を開くアクションを実行|

ソースは下記の通りです。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="828B4C18-AB0B-4593-9F01-C6E2A655AF6B" Name="fileInUseTest01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="09c6f889-7d45-4314-833d-d0975b58d3c7">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <UIRef Id="WixUI_Minimal"/>

        <CustomAction Id="aCmdExec" Directory="INSTALLFOLDER" Execute="deferred" Impersonate="yes" Return="asyncNoWait"
                 ExeCommand="&quot;[#fExe]&quot; &quot;[#fDat]&quot;" />
        <InstallExecuteSequence>
            <Custom Action="aCmdExec" Before="InstallFinalize">not Installed</Custom>
        </InstallExecuteSequence>

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

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

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{91A029CA-ED39-48C0-9FB7-2484876A9E21}">
                <File Id="fExe" Source="keepFileOpen.exe" KeyPath="yes"/>
                <File Id="fDat" Source="Product.wxs"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

インストールを実行すると管理者権限でダイアログとコマンドプロンプトが開くので、そのままにしてインストールを終了します。

アンインストールを始めると処理の起動方法にかかわらず、下記のようにダイアログが表示され、ユーザーが開いているファイルを閉じるまでアンインストールが阻止されます。[^3]
![fileInUseTest01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/407513e8-3419-3281-dff5-fdb993a07c5b.png)

#### perMachineなインストーラーで、ユーザー権限でインストール物を起動したままアンインストール

下記の属性の組み合わせを使用します。

|エレメント|属性|値|意味|
|:--------|:---|:--|:--|
|Package|InstallScope|perMachine|管理者権限でインストール|
|CustomAction|Impersonate|yes|ユーザー権限でインストール物を開くアクションを実行|

前項との違いは、カスタムアクションを実行する`Impersonate`属性を`yes`に変えただけです。このケースでは、インストーラーが頑張って（というか、無理をして）アンインストールを進めます。

- Windows InstallerのデフォルトUIによるアンインストール

「プログラムと機能」から今インストールしたプログラムをアンインストールします。このとき、`keepFileOpen.exe`を起動したままにして、ファイルを開いたままにしておきます。ファイルを削除するフェーズになると、以下のようなFilesInUseのダイアログが表示されます。この時表示されるダイアログのデザインは、MSI内のデザインされたダイアログではなく、Windows Installerがデフォルトで持っているダイアログが使われます。
![fileInUseTest04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/dbe0ec48-cc7e-211b-cacd-7140a7eb41f2.png)

このまま[OK]を押せば、インストーラがアプリを終了し、アンインストールが実行されます。また、`Do not close application`を選択して[OK]を押すと、以下のダイアログが表示され、再起動が促されます。開かれていたファイルは、再起動によりファイルが閉じられた後に自動的に削除されます。[^4]
![fileInUseTest05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/262b9265-bbda-f48d-0695-36f5aab77177.png)

- MSIで定義したダイアログを使ったアンインストール

インストールに使用したmsiを起動すると、MSIで定義したダイアログのデザインが表示されます。ファイルを削除するフェーズに移ると、下記のようなRMFilesInUseダイアログ[^5]が表示されいます。
![fileInUseTest06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/ed41b968-d68d-f835-4558-eeb1d411f0da.png)
機能的には前項のWindows Installerデフォルトのダイアログと同じですが、カスタマイズ可能なダイアログが表示できる点が優れています。このメリットを生かすために、「アプリと機能」からアンインストールしたときも、MSIで定義したダイアログを表示するよう細工をすることがあります。[^6]

`Do not close application`を選択したとき、最後に表示される再起動を促すダイアログも、以下のように少しデザインが違います。
![fileInUseTest07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/76eaadb7-3596-8336-b9c2-53c9256cea20.png)
このダイアログは、Windows Installerが持っているダイアログです。このダイアログはカスタムできません。

#### perUserなインストーラーで、ユーザー権限でインストール物を起動したままアンインストール

私自身はperUserなインストーラーをほとんど作ったことがありませんが、試してみました。結果は、「perMachineなインストーラーでユーザー権限でプログラムを開いているとき」と同じで、RMFilesInUseダイアログが開き、インストーラーがファイルを閉じようとする動作になります。

## まとめ

インストール物を開いたまま、アンインストールやメジャーアップグレードした場合、ユーザー権限で開かれていれば、RMFilesInUseダイアログが出て、なんとか処理を続けようとします。管理者権限でインストール物が開かれていると、もはや処理は続行できず、インストーラーはユーザーにファイルを閉じることを促す以上の処理は行いません。


[^1]: 最近は、インストール直後にウィルス対策ソフトがファイルをロックしていることがあり、このダイアログを見る機会が増えたような気がします。
[^2]: RMFilesInUseの定義をMSIから削除するとFilesInUseの方が使われるようになっています。
[^3]: 文面とボタン上の文字があっていないのは、Windows Installerのバグですね。
[^4]: ロックされていたファイルは削除されていますが、ファイルが置かれていたフォルダは残ってしまうようです。
[^5]: RMFilesInUseのRMは、Restart Manager(再起動マネージャー)の略です。FilesInUseダイアログと違い、再起動マネージャーを使うダイアログ、ってことですね。
[^6]: 「アプリと機能」や「プログラムと機能」からアンインストールを実行すると、デフォルトではMSI内に定義したDialogのデザインは使用されず、Windows Installerが用意したUIで実行されます。MSI内に定義したDialogのデザインを使用するには、Uninstallレジストリに独自のアンインストール項目を作成し、別のプロセスからMSIを実行する必要があります。また、インストールに使用したインストーラーを再度実行すると、MSI内に定義したDialogのデザインが使用されます。

[**前の記事へ**](https://qiita.com/tohshima/items/9758f947e9567ad6ffa9)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/71102ac002a45b819eeb)