Windows Installer手引書 Part.30 メンテナンスモードで3択の画面を出さないようにする

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

## メンテナンスモードの標準的な動作

WiX ToolsetのWixUI Dialog Libraryを使ってインストーラーを作成した場合、「アプリと機能」や「プログラムと機能」から製品を選択すると、「修復」「変更」「削除」のいずれかをユーザーに選択させる画面が表示されます。また、インストール済み製品のインストーラーをもう一度起動した場合も、同じ動作になります。以下に、WixUI_Mondoダイアログセットで修復インストールしたときの画面の流れを示します。
![Part30_MenteSeq.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/462303e2-48ca-0003-b639-a91023491849.png)
初心者の頃に、「削除」はともかく「変更」と「修復」は何が違うんだ、と思ったことはないでしょうか。特にFeatureが一つしかなく、ユーザーが必要としている機能を選択する必要がなければ、ここでユーザーを戸惑わせたくない、と思う人は多いようです。その結果、「アプリと機能」や「プログラムと機能」からは「削除」だけ、インストール済み製品のインストーラーをもう一度起動した場合は「修復」（もしくは「削除」）だけにしてほしい、と依頼されることがあります。

## 3つの選択肢の選択結果をMSI側にどうやって渡すのか

3つの選択肢の選択結果をMSI側に渡す仕組みは、それほど複雑ではありません。以下に、3択のダイアログ（MaintenanceTypeDlg）のソースを示します。

```xml:MaintenanceTypeDlg.wxs
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <UI>
            <Dialog Id="MaintenanceTypeDlg" Width="370" Height="270" Title="!(loc.MaintenanceTypeDlg_Title)">
                <Control Id="ChangeButton" Type="PushButton" X="40" Y="65" Width="80" Height="17" ToolTip="!(loc.MaintenanceTypeDlgChangeButtonTooltip)" Default="yes" Text="!(loc.MaintenanceTypeDlgChangeButton)">
                    <Publish Property="WixUI_InstallMode" Value="Change">1</Publish>
                    <Condition Action="disable">ARPNOMODIFY</Condition>
                </Control>
                <Control Id="ChangeText" Type="Text" X="60" Y="85" Width="280" Height="20" Text="!(loc.MaintenanceTypeDlgChangeText)">
                    <Condition Action="hide">ARPNOMODIFY</Condition>
                </Control>
                <Control Id="ChangeDisabledText" Type="Text" X="60" Y="85" Width="280" Height="20" NoPrefix="yes" Text="!(loc.MaintenanceTypeDlgChangeDisabledText)" Hidden="yes">
                    <Condition Action="show">ARPNOMODIFY</Condition>
                </Control>
                <Control Id="RepairButton" Type="PushButton" X="40" Y="118" Width="80" Height="17" ToolTip="!(loc.MaintenanceTypeDlgRepairButtonTooltip)" Text="!(loc.MaintenanceTypeDlgRepairButton)">
                    <Publish Property="WixUI_InstallMode" Value="Repair">1</Publish>
                    <Condition Action="disable">ARPNOREPAIR</Condition>
                </Control>
                <Control Id="RepairText" Type="Text" X="60" Y="138" Width="280" Height="30" Text="!(loc.MaintenanceTypeDlgRepairText)">
                    <Condition Action="hide">ARPNOREPAIR</Condition>
                </Control>
                <Control Id="RepairDisabledText" Type="Text" X="60" Y="138" Width="280" Height="30" NoPrefix="yes" Text="!(loc.MaintenanceTypeDlgRepairDisabledText)" Hidden="yes">
                    <Condition Action="show">ARPNOREPAIR</Condition>
                </Control>
                <Control Id="RemoveButton" Type="PushButton" X="40" Y="171" Width="80" Height="17" ToolTip="!(loc.MaintenanceTypeDlgRemoveButtonTooltip)" Text="!(loc.MaintenanceTypeDlgRemoveButton)">
                    <Publish Property="WixUI_InstallMode" Value="Remove">1</Publish>
                    <Condition Action="disable">ARPNOREMOVE</Condition>
                </Control>
                <Control Id="RemoveText" Type="Text" X="60" Y="191" Width="280" Height="20" NoPrefix="yes" Text="!(loc.MaintenanceTypeDlgRemoveText)">
                    <Condition Action="hide">ARPNOREMOVE</Condition>
                </Control>
                <Control Id="RemoveDisabledText" Type="Text" X="60" Y="191" Width="280" Height="20" NoPrefix="yes" Text="!(loc.MaintenanceTypeDlgRemoveDisabledText)" Hidden="yes">
                    <Condition Action="show">ARPNOREMOVE</Condition>
                </Control>
                <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
                <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Disabled="yes" Text="!(loc.WixUINext)" />
                <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
                    <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
                </Control>
                <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.MaintenanceTypeDlgBannerBitmap)" />
                <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
                <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
                <Control Id="Title" Type="Text" X="15" Y="6" Width="340" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.MaintenanceTypeDlgTitle)" />
                <Control Id="Description" Type="Text" X="25" Y="23" Width="340" Height="20" Transparent="yes" NoPrefix="yes" Text="!(loc.MaintenanceTypeDlgDescription)" />
            </Dialog>
        </UI>
    </Fragment>
</Wix>
```

選択肢となる３つのボタンは、Idが`ChangeButton`、`RepairButton`、`RemoveButton`のControlエレメントで実装されています。その子要素であるPublishエレメントの中で、WixUI_InstallModeプロパティに値を設定しています。対応するボタンに応じて、`Change`、`Repair`、`Remove`のいずれかが設定されていることが分かります。こういったことから、このダイアログを使わずとも、WixUI_InstallModeプロパティにいずれかの値を設定してやれば、直接動作を制御できることがわかります。

## ようこそ画面と3択の画面をスキップして、メンテナンスモードで削除だけできるようにする

削除だけにするなら、WixUI_InstallModeプロパティの値は常に`Remove`にすればよいので、実装はそれほど複雑になりません。最初に、メンテナンスモードでは「ようこそ画面」ではなく、「削除準備完了画面」から始まるように変更します。MSIファイルで最初に表示する画面は、InstallUISequenceテーブルに定義してあります。以下に、WixUI_Mondoダイアログセットを使って作成したMSIファイルのInstallUISequenceテーブルを示します。
![Part30_WixUI_Mondo_UISeq.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/ed32f749-bc2d-6e2e-367a-876da971b24e.png)
インストーラーが起動すると、上図の赤で囲んだ３つのダイアログのいずれかが開きます。メンテナンスモードでは、コンディションが評価された結果、MaintenanceWelcomeDlgが表示されます。WiX Toolsetのソースのうち、この項目に対応する記述を抜粋します。

```xml:MaintenanceWelcomeDlg.wxsより
<InstallUISequence>
    <Show Dialog="MaintenanceWelcomeDlg" Before="ProgressDlg" Overridable="yes">Installed AND NOT RESUME AND NOT Preselected AND NOT PATCH</Show>
</InstallUISequence>
```

ここで注目すべきところは、showエレメントの` Overridable="yes"`です。これが付いているおかげで、別の場所で内容を上書きできるようになります。これは、「最初に表示するダイアログを変更できる」、ということを意味します。メンテナンスモード時に最初に表示するダイアログをVerifyReadyDlgに変更するには、下記のようにします。InstallUISequenceにVerifyReadyDlgのShowエレメントを追加し、MaintenanceWelcomeDlgのコンディションをコピーしてきます。そして、MaintenanceWelcomeDlgのコンディションを`False`に変更して、この画面を使わないことを明示します。

```xml
<Property Id="WixUI_InstallMode" Value="Remove"/>
<InstallUISequence>
    <Show Dialog="VerifyReadyDlg" Before="MaintenanceWelcomeDlg">Installed AND NOT RESUME AND NOT Preselected AND NOT PATCH</Show>
    <Show Dialog="MaintenanceWelcomeDlg" Before="ProgressDlg">False</Show>
</InstallUISequence>
```

「アプリと機能」や「プログラムと機能」からアンインストールした時にもMSIが持つダイアログセットを使用するように変更して、常にアンインストールになることを確認します。そのために、下記のカスタムアクションを追加します。

```xml
<Property Id="reg_exe" Value="reg"/>
<CustomAction Id="setWinInstallerReg" Property="reg_exe"
    ExeCommand="add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[ProductCode] /v WindowsInstaller /t REG_DWORD /d 0 /f /reg:32"
    Impersonate="no" Return="asyncNoWait" Execute="deferred"/>
<InstallExecuteSequence>
    <!-- Windows Installerが設定したUninstallキー下の情報のうち、WindowsInstaller項目を「0」に変更し、アンインストール時にWiXのダイアログを表示するように変える -->
    <Custom Action="setWinInstallerReg" After="RegisterProduct">NOT Installed</Custom>
</InstallExecuteSequence>
```

これで、インストール済み製品のインストーラーをもう一度起動した場合も、「アプリと機能」や「プログラムと機能」からアンインストールした場合も、ようこそ画面と3択の画面をスキップし、直接「削除」できるようになります。ビルドできる完全なソースを以下に示します。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Product Id="B0B1E980-A90A-419E-863F-F4B834619A4B" Name="Part30_03" Language="1041" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="5390170f-8bef-4ddd-b8ef-4c802478ab03">
        <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <UIRef Id="WixUI_Mondo"/>

        <!-- メンテナンスモードでは常に削除になるよう変更 -->
        <Property Id="WixUI_InstallMode" Value="Remove"/>
        <InstallUISequence>
            <!-- メンテナンスモードで最初に表示する画面をVerifyReadyDlgに変更する -->
            <Show Dialog="VerifyReadyDlg" Before="MaintenanceWelcomeDlg">Installed AND NOT RESUME AND NOT Preselected AND NOT PATCH</Show>
            <Show Dialog="MaintenanceWelcomeDlg" Before="ProgressDlg">False</Show>
        </InstallUISequence>

        <!-- Windows Installerが設定したUninstallキー下の情報のうち、WindowsInstaller項目を「0」に変更し、アンインストール時にWiXのダイアログを表示するように変える -->
        <Property Id="reg_exe" Value="reg"/>
        <CustomAction Id="setWinInstallerReg" Property="reg_exe"
            ExeCommand="add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[ProductCode] /v WindowsInstaller /t REG_DWORD /d 0 /f /reg:32"
            Impersonate="no" Return="asyncNoWait" Execute="deferred"/>
        <InstallExecuteSequence>
            <Custom Action="setWinInstallerReg" After="RegisterProduct">NOT Installed</Custom>
        </InstallExecuteSequence>

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

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

    <Fragment>
        <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{90C5B3AC-3722-470F-A632-BEDD0762C7E7}">
                <File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
            </Component>
        </ComponentGroup>
    </Fragment>
</Wix>
```

## インストール済みのバージョンを再度インストールした時に「修復インストール」に固定する

先ほどの例では「常に削除」という動作でしたが、今度は「インストール済み製品のインストーラーをもう一度起動した場合は修復」に変えてみます。そのためには、メンテナンスモードに入った時、インストーラーから起動したのか、「アプリと機能」や「プログラムと機能」から起動したのか、判断する必要があります。手がかりを求めてマイクロソフトのドキュメントを探しましたが、MSIの内部から「どちらから自分が起動されたか」判断できる情報は、OriginalDatabaseプロパティくらいしかありませんでした。どのパスにあるMSIファイルが使用されているのか、このプロパティで知ることができますが、MSIの機能で文字列解析するのは容易でないため利用することをあきらめました。そこで、MSIの起動前に外部で判断し、パブリックプロパティでMSIに結果を渡すことを考えます。しかし、インストール済みのバージョンを再度インストールする時に、インストーラーをコマンドプロンプトや外部のプログラムから起動した場合は、msiexecのコマンドラインに指定したパブリックプロパティを、MSI内から参照できないという謎仕様がある[^1]ことが分かりました。

結局、シンプルで採用に値する方法は、Windows Installerがレジストリに登録するアンインストール用のコマンドライン文字列を改造し、識別用のパブリックプロパティをMSIに渡すようにする方法となりました。このレジストリ項目の名前はUninstallStringで、下記のキーの下にあります（ここで、[製品コード]は`{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`の書式のGUIDを示す）。

- 64bit OSのとき  
`HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\[製品コード]`
- 32bit OSのとき  
`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[製品コード]`

WiX Toolsetで作ったインストーラーの場合、UninstallStringの内容は下記のようになっています[^2]。
`MsiExec.exe /I[製品コード]`
Windows Installerがインストール時にこれを登録した後、下記のように書き換えます。
`MsiExec.exe /I[製品コード] BOOTFROMARP=1`
MSI内ではメンテナンスモード時に、BOOTFROMARPプロパティが定義されていたら「アプリと機能」や「プログラムと機能」から起動されたと判定し、定義されていなければインストール済みのバージョンを再度インストールしたと判定します。以上を実装すると、下記のようになります。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Product Id="B0B1E980-A90A-419E-863F-F4B834619A4B" Name="secundBootTest" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="1a8e05fe-93a4-4905-b78a-acfb0da08bc2">
        <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <UIRef Id="WixUI_Mondo"/>

        <CustomAction Id="setRepairMode" Property="WixUI_InstallMode" Value="Repair"/>
        <CustomAction Id="setRemoveMode" Property="WixUI_InstallMode" Value="Remove"/>
        <InstallUISequence>
            <!-- インストール済みで「プログラムと機能」からの起動でないとき、WixUI_InstallModeプロパティにRepairを格納 -->
            <Custom Action="setRepairMode" Before="VerifyReadyDlg">Installed AND NOT BOOTFROMARP</Custom>
             <!-- インストール済みで「プログラムと機能」からの起動であるとき、WixUI_InstallModeプロパティにRemoveを格納 -->
            <Custom Action="setRemoveMode" Before="VerifyReadyDlg">Installed AND BOOTFROMARP</Custom>
            <!-- インストール済みのときは、最初にVerifyReadyDlgを表示するよう変更 -->
            <Show Dialog="VerifyReadyDlg" Before="MaintenanceWelcomeDlg">Installed</Show>
            <!-- MaintenanceWelcomeDlgを使わないよう変更 -->
            <Show Dialog="MaintenanceWelcomeDlg" Before="ProgressDlg">False</Show>
        </InstallUISequence>

        <Property Id="reg_exe" Value="reg"/>
        <CustomAction Id="setWinInstallerReg" Property="reg_exe"
            ExeCommand="add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[ProductCode] /v WindowsInstaller /t REG_DWORD /d 0 /f /reg:32"
            Impersonate="no" Return="asyncNoWait" Execute="deferred"/>
        <CustomAction Id="addBootFromArp" Property="reg_exe" 
            ExeCommand="add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[ProductCode] /v UninstallString /t REG_EXPAND_SZ /d &quot;MsiExec.exe /I[ProductCode] BOOTFROMARP=1&quot; /f /reg:32" 
            Impersonate="no" Return="asyncNoWait" Execute="deferred"/>
        <InstallExecuteSequence>
            <!-- Windows Installerが設定したUninstallキー下の情報のうち、WindowsInstaller項目を「0」に変更し、アンインストール時にWiXのダイアログを表示するように変える -->
            <Custom Action="setWinInstallerReg" After="RegisterProduct">NOT Installed</Custom>
            <!-- Windows Installerが設定したUninstallキー下の情報のうち、「プログラムと機能」から起動したときのコマンドラインにBOOTFROMARPプロパティ設定を追加 -->
            <Custom Action="addBootFromArp" After="setWinInstallerReg">NOT Installed</Custom>
        </InstallExecuteSequence>

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

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

    <Fragment>
        <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{90C5B3AC-3722-470F-A632-BEDD0762C7E7}">
                <File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
            </Component>
        </ComponentGroup>
    </Fragment>
</Wix>
```

## インストール済みか検出してREINATALLモードを使う手もある

もう一つ、別の方法として、msiexecを起動するセットアップランチャーを用意し、その中で「初めて起動したのか」、「インストール済み製品のインストーラーをもう一度起動したのか」判断し、後者ならREINATALLモードを使って上書きインストールさせる方法が考えられます。

初めて起動したなら、
`msiexec /i Part30_01.msi`
のように。インストール済み製品のインストーラーをもう一度起動したのなら、
`msiexec REINSTALL="ALL" REINSTALLMODE="omus" /i Part30_01.msi`
のように起動の方法を切り替えます。

具体的な方法は、次回セットアップランチャーの説明の中で示したいと思います。

[^1]: どういうロジックが働いてこのような結果になってしまうのか、是非知りたいところです。
[^2]: msiexecのアンインストール用のコマンドラインスイッチは`/x`ですが、わざわざ`/i`を使っているのはMSI内のUIを使う意思を感じます。しかし、レジストリのWindowsInstaller項目が`1`になっているために、このままでは最小限のUIしか出ません。なぜこうなっているのか・・・不思議です。

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