プログラムのパスを検索する

あるプログラムから外部プログラムを起動する必要があったのですが,この際に外部プログラムのパス(インストールされたディレクトリ)を取得するのはどうすれば良いのだろう?と言う事が問題となりました.そこで,ちょっとぐぐってみたところ,どうやら Windows Installer を用いてインストールされたものであれば msi.dll で提供される API を用いる事で分かるようです.

You can use the Windows Installer to get the path of the installed Office 2000 application. Follow the steps given below to create a console application that reports the directory of an installed Office application.

http://support.microsoft.com/kb/234788/en

プログラム毎にコンポーネントID と呼ばれる値が設定されているようで,この値が分かっているならこの値からパスを知ることができるようです(ただし,コンポーネントID はバージョン等が異なると値が違う?).

コンポーネントID が分からない場合は,MsiEnumComponents を用いてユーザのローカルマシンにインストールされているプログラムのコンポーネントID を順に取得していき,その値から得られるパスのファイル名辺りで判断する事になりそうです.

C# でざっと書いてみると下記のようになるでしょうか.

Win32 API のインポート

まず,Win32 API を使用するので下記のようにインポートするための関数宣言を書きます.尚,msi.dll を使用するには,参照の追加で COM → Microsoft Windows Installer Object Library を追加する必要があるようです.

using System;
using System.Runtime.InteropServices;

namespace WinApi {
    public abstract class Application {
        private const System.String MSI_DLL = "msi.dll";

        /* ----------------------------------------------------------------- */
        /*
         *  UINT MsiEnumComponents (
         *    DWORD  iComponentIndex,
         *    LPTSTR lpComponentBuf
         *  );
         *  
         *  http://msdn.microsoft.com/en-us/library/aa370097(VS.85).aspx
         */
        /* ----------------------------------------------------------------- */
        [DllImport(MSI_DLL, CharSet = CharSet.Auto, SetLastError = true)]
        public extern static UInt32 MsiEnumComponents(UInt32 iComponentIndex,
            System.Text.StringBuilder lpComponentBuf);

        /* ----------------------------------------------------------------- */
        /*
         *  UINT MsiGetProductCode (
         *    LPCTSTR szComponent,  // component ID
         *    LPTSTR  lpProductBuf  // product code for the client product
         *  );
         *  
         *  http://msdn.microsoft.com/en-us/library/aa370129(VS.85).aspx
         */
        /* ----------------------------------------------------------------- */
        [DllImport(MSI_DLL, CharSet = CharSet.Auto, SetLastError = true)]
        public extern static UInt32 MsiGetProductCode(String szComponent,
            System.Text.StringBuilder lpProductBuf);

        /* ----------------------------------------------------------------- */
        /*
         *  INSTALLSTATE MsiGetComponentPath (
         *    LPCTSTR szProduct,    // product code for the client product
         *    LPCTSTR szComponent,  // component ID
         *    LPTSTR  lpPathBuf,
         *    DWORD*  pcchBuf
         *  );
         *  
         *  http://msdn.microsoft.com/en-us/library/aa370112(VS.85).aspx
         */
        /* ----------------------------------------------------------------- */
        [DllImport(MSI_DLL, CharSet = CharSet.Auto, SetLastError = true)]
        public extern static Int32 MsiGetComponentPath(String szProduct, String szComponent,
            System.Text.StringBuilder lpPathBuf, ref UInt32 pcchBuf);
    }
}
サンプルプログラム
using System;

namespace Clown {
    public abstract class Path {
        /* ----------------------------------------------------------------- */
        /*
         *  Find
         *  
         *  filename に対応するパスを検索する.ignore_case が true の場合
         *  大文字/小文字を区別しない.対応するパスが見つからなかった場合は
         *  null を返す.
         *  
         *  Fine() メソッドで検索可能なパスは,Windows Installer 経由で
         *  インストールされたプログラムのみ.
         */
        /* ----------------------------------------------------------------- */
        public static System.String Find(System.String filename, bool ignore_case = false) {
            if (ignore_case) filename = filename.ToLower();
            System.Text.StringBuilder component = new System.Text.StringBuilder(64);

            // ERROR_NO_MORE_ITEMS = 259
            for (UInt32 i = 0; WinApi.Application.MsiEnumComponents(i, component) != 259; i++) {
                System.String path = FindFromComponentId(component.ToString());
                if (path == null) continue;
                System.String compare = (ignore_case) ?
                    System.IO.Path.GetFileName(path).ToLower() :
                    System.IO.Path.GetFileName(path);
                if (compare == filename) return path;
            }
            return null;
        }

        /* ----------------------------------------------------------------- */
        /*
         *  FindFromComponentId
         *  
         *  指定されたコンポーネントID に対応するパスを検索する.
         *  見つからなかった場合は null を返す.
         */
        /* ----------------------------------------------------------------- */
        public static System.String FindFromComponentId(System.String component) {
            System.Text.StringBuilder product = new System.Text.StringBuilder(64);
            if (WinApi.Application.MsiGetProductCode(component, product) != 0) return null;

            UInt32 len = 2048;
            while (true) {
                UInt32 result = len;
                System.Text.StringBuilder path = new System.Text.StringBuilder((int)len);
                if (WinApi.Application.MsiGetComponentPath(product.ToString(), component, path, ref result) <= 0) break;
                else if (result < len) return path.ToString();
                else len *= 2; // バッファ不足
            }

            return null;
        }
    }
}
namespace SearchComponent {
    class Program {
        static void Main(string[] args) {
            for (int i = 0; i < args.Length; i++) {
                System.String path = Clown.Path.Find(args[i], true);
                if (path == null) System.Console.WriteLine("{0} is not found", args[i]);
                else System.Console.WriteLine(path);
            }
        }
    }
}
実行結果
[clown@stinger Release]$ ./SearchComponent AcroRd32.exe foo.exe firefox.exe
C:\Program Files (x86)\Adobe\Reader 9.0\Reader\AcroRd32.exe
foo.exe is not found
firefox.exe is not found

ただ,この方法で探せるプログラムも結構限られるようで(Firefox は無理だった),Windows Installer を経由していないプログラムのインストール先を調べるにはまた別の方法が必要となるようです.

トップレベルの名前空間を何にしようかなぁと言うのはいつも考えてしまう部分です.面倒なので,特に決まったものがなければ clown を使う事にしました.