作成日: 2007/09/01

Windows Vista と Visual Studio 2005 セットアッププロジェクト

概要

Visual Studio 2005 のセットアッププロジェクトでインストーラを作成したはいいのですが、 インストール中に行うファイアウォールの設定が、Windows Vista ではどうしてもうまくできませんでした。 ですが、管理者特権でインストーラを起動することでうまく設定できるようになりました。

インストールするプログラムを作成する

簡単なプログラムを作成します。インストール中にファイアウォールの設定をするので、 うまく設定できたことを確認できるように、サーバソケットを扱うプログラムにします。
なお、参考までに以下にウィザードでの設定画面の一部を示しますが、設定値はサンプルのプログラム用であり、 参考にする必要はありません。お好きに設定してください。
また、ついでなのでと書いてみたものの本旨ではないのに説明がだらだらと長いです。 インストールするプログラムはわざわざ作らなくても構わないので、ここは読み飛ばしてくださって構いません。

Visual Stduio 2005 (以下 VS) で [プロジェクトの種類] を [MFC]、テンプレートを [MFC アプリケーション] にしてプロジェクトを作成します。 ここではプロジェクト名を Sample にしました。

新しいプロジェクト

[アプリケーションの種類] を[ダイアログベース]、[ユニコードライブラリを使用する] のチェックを外し、 [MFC の使用法] を [スタティックライブラリで MFC を使用] にします。

アプリケーションの種類

[メインフレームのスタイル] から [システムメニュー]、[バージョン情報] のチェックを外します。

ユーザーインターフェース機能

[高度な機能] から [ActiveX コントロール] のチェックを外します。

高度な機能

作成したプロジェクトのダイアログリソースを変更します。
デフォルトで作成されている [OK]、[キャンセル] ボタンのクリック時のイベントハンドラを作成した後、[OK] ボタンは削除し、 [キャンセル] ボタンのキャプションを [閉じる] に変更します。
[開始] ボタン、[停止] ボタンを作成し、リソース ID をそれぞれ、[IDC_START]、[IDC_STOP] に変更します。

作成したダイアログ

[開始] ボタンを [変数の追加] で、コントロール変数 m_start として定義します。
同様に [停止]、[閉じる] ボタンも名前をそれぞれ m_stop、m_cancel としてコントロール変数に定義します。

コントロール変数の追加

CSampleDlg にメンバ関数を追加します。シグネチャは static UINT ThreadFunc(void* pParam) とします。

メンバ関数の追加

CSampleDlg にメンバ変数を追加します。型は BOOL、名前を m_stopThread にします。
同様に、型を CWinThread*、名前を m_pThread としてメンバ変数を追加します。

メンバ変数の追加

コードを書きます。なお、ソケット操作は怪しげな自作のクラスを使っています。詳細はソースを見てください。
※まったくの余談ですが、ウィザードを使ってメンバ変数を追加すると、 ちゃんとコンストラクタに初期化のコードが追加されるのですね。知らなかったのです。
いつもリソースを作成したら後はエディタでごりごりやってたので気がつきませんでした。VS も進化してるのですね。

SampleDlg.cpp のエディタで変更した付近のみ抜粋

#include "SocketLib.h"
BOOL CSampleDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // このダイアログのアイコンを設定します。アプリケーションのメイン ウィンドウがダイアログでない場合、
    //  Framework は、この設定を自動的に行います。
    SetIcon(m_hIcon, TRUE);            // 大きいアイコンの設定
    SetIcon(m_hIcon, FALSE);        // 小さいアイコンの設定

    // TODO: 初期化をここに追加します。
    m_start.EnableWindow(TRUE);
    m_stop.EnableWindow(FALSE);
    m_cancel.EnableWindow(TRUE);

    return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}
void CSampleDlg::OnBnClickedOk()
{
    // [Enter] 押下で閉じないように。
    ////OnOK();
}

void CSampleDlg::OnBnClickedCancel()
{
    // [Esc] 押下に備える。
    if (m_pThread) {
        return;
    }

    OnCancel();
}

void CSampleDlg::OnBnClickedStart()
{
    m_start.EnableWindow(FALSE);
    m_stop.EnableWindow(TRUE);
    m_cancel.EnableWindow(FALSE);

    // スレッドを開始する
    m_stopThread = FALSE;
    m_pThread = AfxBeginThread(
                        ThreadFunc,
                        this,
                        THREAD_PRIORITY_NORMAL,
                        0,
                        CREATE_SUSPENDED,
                        0
                    );
    ASSERT(m_pThread);
    m_pThread->m_bAutoDelete = FALSE;
    m_pThread->ResumeThread();
}

void CSampleDlg::OnBnClickedStop()
{
    // スレッドを停止する
    m_stopThread = TRUE;
    WaitForSingleObject(m_pThread->m_hThread, INFINITE);
    delete m_pThread;
    m_pThread = 0;

    m_start.EnableWindow(TRUE);
    m_stop.EnableWindow(FALSE);
    m_cancel.EnableWindow(TRUE);
}

UINT CSampleDlg::ThreadFunc(void* pParam)
{
    CSampleDlg* pDlg = (CSampleDlg*) pParam;

    CSocketLib svsd;
    BOOL rc;

    // サーバソケットを作成する
    rc = svsd.CreateServerSocket(5000, 5);
    ASSERT(rc);

    while (!pDlg->m_stopThread) {
        // 1 秒ごとに抜けて終了が指示されていないかチェックする。
        rc = svsd.ReadSelect(1, 0);
        if (!rc) {
            continue;
        }

        CSocketLib clsd;

        // クライアントからの接続を受け付けて、
        // 文字列を流し込んで、閉じる。
        rc = svsd.Accept(&clsd);
        if (!rc) {
            continue;
        }
        const char* msg = "Hello!";

        clsd.Send(msg, (int) strlen(msg));

        clsd.Close();
    }

    svsd.Close();

    return 0;
}

Vista で初めてこのプログラムを実行し [開始] ボタンをクリックした時に、以下のような警告がでます。
※リソースがデフォルトのままなので、名前とか発行元が TODO 云々になってます。ご勘弁を。。。

Windows セキュリティの重要な警告

このプログラムを使ってインストーラを作成しますが、インストーラでファイアウォールの設定を行えば、 実行時にこの警告が表示されることはありません。
ということで、やっと、本題に入ります。長々とすみません。。。

セットアッププロジェクトを作成する

セットアッププロジェクトはインストールするプログラムと同じソリューションに追加作成した方がビルドの時などに便利です。
ここでも、上のサンプルプログラムのソリューションに追加作成します。

[プロジェクトの種類] を [セットアップと配置]、[テンプレート] を [セットアッププロジェクト] にしてプロジェクトを作成します。 ここではプロジェクト名を SetupSample にしました。

新しいプロジェクトの追加

セットアッププロジェクトにインストールするファイルを追加します。
プロジェクトを右クリックして、[追加] - [プロジェクト出力] を選びます。

プロジェクト出力の追加

表示される [プロジェクト出力グループの追加] で [プロジェクト] を [Sample] にし、[プライマリ出力] を選んで [OK] をクリックします。

プライマリ出力を選択

インストールファイルを追加した際に、[見つかった依存関係] にいくつかファイルが追加される場合があります。 サンプルプログラムの場合、comdlg32.dll が追加されました。依存関係に追加されたファイルはそのままにしておくと、 一緒にインストーラに含まれてしまいます。
サンプルプログラムでは、comdlg32.dll はインストーラには含めたくないので、依存関係から除外します。
[見つかった依存関係] を右クリックして、[除外] を選びます。

comdlg32.dll を除外

comdlg32.dll に除外されたことを表すマークが表示されます。

comdlg32.dll が除外された

インストール時にデスクトップにショートカットを作成するように設定します。
セットアッププロジェクトを右クリックして、[表示] - [ファイルシステム] を選びます。

ファイルシステムを表示

最初に、先ほど追加した Sample の プライマリ出力が [アプリケーションフォルダ] に追加されているのを確認します。

アプリケーションフォルダ

次に、[ユーザーのデスクトップ]をクリックしてから、右クリックして、[新しいショートカットの作成] を選びます。

新しいショートカットの作成

[プロジェクトから項目を選択] で、[アプリケーションフォルダ] をダブルクリックします。

プロジェクトから項目を選択

表示される [Sample (アクティブ) のプライマリ出力] を選んで [OK] をクリックします。

Sample (アクティブ) のプライマリ出力を選択

作成された、ショートカットの名前を [サンプルプログラム] に変更します。

ショートカット名の変更

作成したショートカットのアイコンを変更するように設定します。
ショートカットのプロパティで [Icon] から [参照...] を選びます。

参照を選択

表示される [アイコン] ダイアログで [参照] を選びます。

アイコンダイアログ

表示される [プロジェクトから項目を選択] で [アプリケーションフォルダ] をダブルクリックします。

プロジェクトから項目を選択

表示される [アプリケーションフォルダ] で [ファイルの種類] を [実行可能ファイル (*.exe)] にし、 表示される [Sample (アクティブ) のプライマリ出力] を選んで [OK] をクリックします。

Sample (アクティブ) のプライマリ出力を選択

[アイコン] ダイアログに表示されるアイコンから適当なものを選んで [OK] をクリックします。
※もし、[アイコン] ダイアログにアイコンが表示されない場合、対象となるプロジェクトをビルドしてから、もう一度やり直してください。

アイコンを選択する

インストーラとしては一応完成ですが、まだファイアウォールの設定ができるようになっていません。 以下では、ファイルウォールの設定のためにファイルを追加していきます。

ファイアウォールの設定のためのスクリプトを作成し、追加する

ファイアウォールの設定は netsh コマンドを使い、コマンドの実行はスクリプトで行います。 インストール中にスクリプトを実行するためにはカスタム動作にスクリプトを登録する必要があります。
実行するコマンドは単純なものなので、バッチファイルでも良さそうなものですが、 カスタム動作がバッチファイルを受け付けてくれず、仕方なくスクリプトを書くことにしました。 なお、スクリプトは VBScript で Windows Script Host を利用していますが、少しだけ通常のスクリプトとは違う点があります。

  • WScript.CreateObject() のようにいきなり WScript を参照するメソッドは使えない。
    代わりに CreateObject() を使う必要があります。

  • インストーラからプロパティ値 CustomActionData を受け取ることができる。
    カスタム動作の定義で、実行するスクリプトへの引数として CustomActionData を渡すことができます。

スクリプトはインストール用とアンインストール用の 2 つ作りました。 それぞれ、CustomActionData プロパティで対象のプログラムのパスを受け取り、 そのプログラムを netsh でファイアウォールに追加または削除します。
詳細はスクリプトのソースを参照ください。

作成したスクリプトはインストール用を InstallFirewall.vbs、アンインストール用を UninstallFirewall.vbs と名前を付け、 SetupSample プロジェクトのフォルダにコピーしました。

作成したスクリプトをセットアッププロジェクトに追加します。
SetupSample プロジェクトを右クリックし、[追加] - [ファイル] を選びます。

ファイルをプロジェクトに追加

先ほどコピーした InstallFirewall.vbs、UninstallFirewall.vbs を選び、[開く] をクリックします。

ファイルを選択する

追加したファイルは [ファイルシステム] の [アプリケーションフォルダ] に追加されます。

ファイルシステム

カスタム動作にスクリプトを登録します。
SetupSample プロジェクトを右クリックし [表示] - [カスタム動作] を選びます。

カスタム動作を表示する

最初にインストール時にファイアウォールの設定をするスクリプトを追加します。
[インストール] を右クリックして、[カスタム動作の追加] を選びます。

カスタム動作の追加

表示される [プロジェクトから項目を選択] で [アプリケーションフォルダ] をダブルクリックします。

プロジェクトから項目を選択

[InstallFirewall.vbs] を選び [OK] をクリックします。

InstallFirewall.vbs を選択

追加した [InstallFirewall.vbs] の CustomActionData にプログラムのパスを追加します。
[InstallFirewall.vbs] のプロパティの CustomActionData に "[TARGETDIR]Sample.exe" と設定します。

プロパティを設定する

次にアンインストール時にファイアウォールの設定を削除するスクリプトを追加します。
[アンインストール] を右クリックして、[カスタム動作の追加] を選びます。

カスタム動作の追加

表示される [プロジェクトから項目を選択] で [アプリケーションフォルダ] をダブルクリックします。

プロジェクトから項目を選択

[UninstallFirewall.vbs] を選び [OK] をクリックします。

UninstallFirewall.vbs を選択

追加した [UninstallFirewall.vbs] の CustomActionData にプログラムのパスを追加します。
[UninstallFirewall.vbs] のプロパティの CustomActionData に "[TARGETDIR]Sample.exe" と設定します。

プロパティを設定する

同様に [ロールバック] にも [アンインストール] と同じ設定をします。

ロールバックスクリプトを追加する

ビルドして作成される SetupSample.msi がインストーラになります。
これでファイアウォールの設定を行うようになりました。ですが、Vista でインストールする際には、ファイアウォールの設定ができません。
Administrator でログオンして SetupSample.msi を実行するだけでは権限が足らないようです。
通常のプログラムであれば、右クリックして [管理者として実行] することで管理者特権で実行できるのですが、 MSI セットアップモジュールを右クリックしても [管理者として実行] は表示されません。
コマンドプロンプトを [管理者として実行] して、プロンプト内から SetupSample.msi を実行すれば、 管理者特権で実行され、ファイアウォールの設定も行われるのですが、やっぱり不便です。
以下では、SetupSample.msi を起動するためのプログラムを作成し、それを管理者特権で起動するようにします。

MSI セットアップモジュールの起動プログラムを作成する

セットアッププロジェクトと同じソリューションに起動プログラムを追加作成します。

[プロジェクトの種類] を [MFC]、[テンプレート] を [MFC アプリケーション] にしてプロジェクトを作成します。 プロジェクト名は InstallSample にしました。

新しいプロジェクトの追加

[アプリケーションの種類] を[ダイアログベース]、[ユニコードライブラリを使用する] のチェックを外し、 [MFC の使用法] を [スタティックライブラリで MFC を使用] にします。

アプリケーションの種類

[メインフレームのスタイル] から [システムメニュー]、[バージョン情報] のチェックを外します。

ユーザーインターフェース機能

[高度な機能] から [ActiveX コントロール] のチェックを外します。

高度な機能

作成した InstallSample はダイアログベースになっています。 ですが、SetupSample.msi を起動するだけなので、ダイアログは不要です。
関連するリソースとソースを削除します。

InstallSample のリソースビューでダイアログリソースを選んで削除します。

ダイアログリソースの削除

InstallSample のソリューションエクスプローラで InstallSampleDlg.cpp を削除します。

InstallSampleDlg.cpp の削除

[削除] を選びます。

削除を選ぶ

同様に InstallSampleDlg.h も削除します。

InstallSample.cpp を開き、変更します。

InstallSample.cpp のエディタで変更した付近のみ抜粋

#include "stdafx.h"
#include "InstallSample.h"
//#include "InstallSampleDlg.h"
BEGIN_MESSAGE_MAP(CInstallSampleApp, CWinApp)
//  ON_COMMAND(ID_HELP, &CWinApp::OnHelp)
END_MESSAGE_MAP()
BOOL CInstallSampleApp::InitInstance()
{
    CWinApp::InitInstance();

    const CHAR* setupMsiName = "SetupSample.msi";
    CString msg;

    // カレントディレクトリを得る。
    CHAR modulePath[MAX_PATH * 10];
    DWORD pathLength = GetModuleFileName(AfxGetInstanceHandle(), modulePath, sizeof(modulePath));
    modulePath[pathLength] = '\0';
    CHAR *wp = strrchr(modulePath, '\\');
    if (wp) {
        *wp = '\0';
    }

    CString currentDirectory = modulePath;


    // セットアップモジュールが存在するか確認する。
    SetCurrentDirectory(currentDirectory);

    DWORD attr = GetFileAttributes(setupMsiName);
    if (attr == (DWORD) -1 || (attr & FILE_ATTRIBUTE_DIRECTORY)) {
        msg.Format("セットアップモジュール %s が存在しません。ファイルを確認してから再度実行してください。", setupMsiName);
        AfxMessageBox(msg, MB_OK | MB_ICONINFORMATION);
        return FALSE;
    }

    // コマンドラインを作成する。
    CString commandLine;
    commandLine.Format("msiexec /i %s", setupMsiName);

    // インストーラを起動する。
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    memset(&si, 0, sizeof(si));
    si.cb = sizeof(si);
    memset(&pi, 0, sizeof(pi));

    BOOL rc = CreateProcess(
                        0, (LPSTR) LPCTSTR(commandLine),
                        0, 0,
                        FALSE,
                        CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP,
                        0, LPCTSTR(currentDirectory),
                        &si, &pi
                    );
    if (rc) {
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    } else {
        // msiexec が存在しない場合
        msg.Format("コマンドライン %s が実行できません。", commandLine);
        AfxMessageBox(msg, MB_OK | MB_ICONINFORMATION);
    }

    return FALSE;
}

マニフェストファイルを作成します。大事なのは requestedExecutionLevel の RequireAdministrator で、 このリソースを組み込んだプログラムを管理者特権で実行する必要があることを示します (プログラムのアイコンに盾のマークがつきます)。
作成したマニフェストファイルは InstallSample.manifest と名前を付け、InstallSample の res フォルダにコピーしました。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
    version="1.0.0.0"
    processorArchitecture="X86"
    name="InstallSample"
    type="win32"
/>
<description>InstallSample</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
        <requestedPrivileges>
            <requestedExecutionLevel level="RequireAdministrator" />
        </requestedPrivileges>
    </security>
</trustInfo>
</assembly>

マニフェストファイルを追加します。リソースビューで InstallSample のリソースを右クリックします。

リソースの追加

[リソースの追加] で [インポート] をクリックします。

リソースのインポート

InstallSample.manifest を選んで、[開く] をクリックします。

InstallSample.manifest を選択

カスタムリソースの種類に "RT_MANIFEST" と入力し [OK] をクリックします。

リソースの種類を設定

追加したマニフェストのリソース上の名前は何でも構わないようです。適宜名前を変更してください。

ビルドします。ビルドの際に以下のような表示が出ます。VS が requestedPrivileges を認識していないようですが、無視して構いません。

\res\InstallSample.manifest : manifest authoring warning 81010002: Unrecognized Element "requestedPrivileges" in namespace "urn:schemas-microsoft-com:asm.v3".

ビルドが済んだら InstallSample.exe と SetupSample.msi を同じフォルダにコピーします。
InstallSample.exe を実行すると、UAC の警告が表示されます。ここで [許可] をクリックして実行を継続すると、 管理者特権でインストーラ SetupSample.msi が起動されます。

UAC の警告

インストール後にはファイアウォールの設定に Sample が例外として登録されています。また、Sample.exe を起動して、 [開始] ボタンをクリックしてもファイアウォールに関する警告は表示されません。

ファイアウォールの設定

また、コントロールパネル の [プログラムと機能] からアンインストールを行うとファイアウォールの設定が削除されることも確認してください。

その他

インストール時に [インストールフォルダの選択] でデフォルトが [このユーザーのみ] になっているのを [すべてのユーザー] にするには、SetupSample セットアッププロジェクトのプロパティで [InstallAllUsers] を True にします。

すべてのユーザに設定

SetupSample セットアッププロジェクトをビルドした際に作成される Setup.exe は必須コンポーネントのインストール用ですが、 これが不要の場合、必須コンポーネント用のセットアッププログラムを作成しないようにします。
セットアッププロジェクトのプロパティで、[必須コンポーネント] をクリックします。

必須コンポーネントの設定

表示される [必須コンポーネント] で [必須コンポーネントをインストールするセットアッププログラムを作成する] のチェックを外します。
※この設定は各構成ごとに行う必要があります。通常であれば Debug、Release の 2 つの構成がありますので、 それぞれに設定する必要があります (Debug を使わないのであればわざわざ行うことはありませんが。。。)。

チェックを外す

ファイアウォールの設定を行うスクリプトを隠しファイルとしてインストールする場合は、スクリプトのプロパティで [Hidden] を True にします。

スクリプトのプロパティ

インストーラでタイトル部分に表示されるビットマップを独自のものに変更する場合、 横 500、縦 70 ピクセルのビットマップを作成し、プロジェクトに追加して、SetupSample プロジェクトを右クリックし、 [表示] - [ユーザーインターフェース] を選び、各画面のプロパティ BannerBitmap で [参照] を選び、 追加したビットマップを選びます。

ビットマップを独自のものにする

インストーラのプロダクト名はデフォルトでインストーラプロジェクトと同じ名前になっています。 これを変更するには、SetupSample セットアッププロジェクトのプロパティで [ProcudeName] にプロダクト名を入力します。

プロダクト名を設定する

インストール先のアプリケーションフォルダはデフォルトで [ProgramFilesFolder][Manufacturer]\[ProductName] になっており、 通常だと [Program Files\製造元名\プロダクト名] にインストールされます。 製造元名 を省きたいときは [ファイルシステム] を表示し、[アプリケーションフォルダ] のプロパティで [DefaultLocation] を変更し、[ProgramFilesFolder][ProductName] とします。

製造元名を省く
サンプルソース

上の作業で使ったソース、スクリプト、プロジェクト、ソリューションファイル一式です。

サンプルソース (SampleProject.zip) (53.5 KB)