2020年8月1日土曜日

【Windows】手前ウィンドウのタイトルとフルパスを取得するツール【プロセス】


概要

<手前ウィンドウのタイトル>と、<手前ウィンドウを表示するプロセスのモジュールのフルパス>をコンソール出力するツールを作成した。
動作の様子。500ミリ秒おきにコンソール出力する。


製造

百聞は一見に如かず。
ソースコードの主要な個所を以下に示す。

メイン関数
#include <fcntl.h>
#include <io.h>
#include <stdlib.h>
#include <wchar.h>
#include <windows.h>
#include "process_tools.h"

int main(void) {
  HWND hWnd = NULL;
  DWORD processId = 0;
  HANDLE hProcess = NULL;
  std::vector<std::wstring> moduleNames;
  wchar_t title[MAX_PATH] = {0};

  // Use Unicode.
  _setmode(_fileno(stdout), _O_U8TEXT);

  while (true) {
    // Open process handle.
    // Window handle -> Process ID -> Process handle
    hWnd = GetForegroundWindow();
    GetWindowThreadProcessId(hWnd, &processId);
    hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
                           processId);
    if (hProcess == NULL) {
      return false;
    }

    // Get name of each module.
    tools::EnumModuleNames(hProcess, moduleNames);

    // Get title of window.
    GetWindowText(hWnd, title, MAX_PATH);

    // Show window title and module names.
    wprintf(L"%ls\n", title);
    wprintf(L"%u modules\n", moduleNames.size());
    for (auto& module : moduleNames) {
      wprintf(L"  %ls\n", module.c_str());
    }
    wprintf(L"--------------------------------------------------\n");
    wprintf(L"\n");

    // Close process handle.
    CloseHandle(hProcess);

    // Interval in read.
    Sleep(500);
  }

  return EXIT_SUCCESS;
}

ポイント
  • EXE と DLL
    アプリケーションの EXE だけでなく、アプリケーションがロードした DLL を
    合わせてコンソールに表示する。EnumProcessModulesEx 関数がフルパスの一覧を
    取得する。
  • Unicode 出力について
    ウィンドウテキストが日本語の場合もコンソール出力で困らないように
    以下のステップで標準出力を UTF-8 にする。
    setmode(_fileno(stdout), _O_U8TEXT);

    ※このステップを入れずにコードページを 65001 とし、
     フォントを日本語対応フォント(MS Gothic)とした際は、
     最初の1文字以降が尻切れになる問題が発生した。

     原因を調べていたところ、参考文献4にこの方法を見つけ、
     問題は解決した。

関数ヘッダファイル
#pragma once

#include <wchar.h>
#include <windows.h>
#include <string>
#include <vector>

namespace tools {

constexpr int ARGUMENT_ERROR = -1;

// (中略)

int EnumModuleNames(HANDLE hProcess, std::vector<std::wstring>& moduleNames);

}  // namespace tools

関数ソースファイル
#include "./process_tools.h"
#include <psapi.h>
#include <windows.h>
#include <algorithm>

// (中略)

namespace tools {

// (中略)

//
// @brief <br> Get the module file path of given window.
// @param [in] hProcess process handle.
// @param [out] moduleNames Full path for modules.
// @return Actually read module number.
//
int EnumModuleNames(HANDLE hProcess, std::vector<std::wstring>& moduleNames) {
  if (hProcess == NULL) {
    return ARGUMENT_ERROR;
  }

  // Enumerate modules of process.
  DWORD dwSize = 0;
  EnumProcessModulesEx(hProcess, NULL, 0, &dwSize, LIST_MODULES_ALL);
  std::vector hModules(dwSize / sizeof(HMODULE));
  if (EnumProcessModulesEx(
          hProcess, hModules.data(),
          static_cast(sizeof(HMODULE) * hModules.size()), &dwSize,
          LIST_MODULES_ALL) == 0) {
    return 0;
  }

  // Get file path of modules.
  moduleNames.clear();
  wchar_t strBuf[MAX_PATH] = {0};
  for (auto& hModule : hModules) {
    GetModuleFileNameEx(hProcess, hModule, strBuf, MAX_PATH);
    moduleNames.push_back(strBuf);
  }

  return static_cast<int>(moduleNames.size());
}

}  // namespace tools

ポイント
  • EnumProcessModules を 2回呼び出す
    1回目の呼び出しではプロセスのモジュールの数を取得し、
    2回目の呼び出しではプロセスのモジュールのハンドルを取得する。

    この方法のメリットは、先に数が分かり、動的に配列を準備できることだ。
    この方法のデメリットは、2回の呼び出しに冗長であることと、
    「1回目と2回目の呼び出しの間に、モジュールの数が変わらないの?」
    という懸念があることだ。

    ※尚、MSDN では決め打ちで十分に大きな固定長配列を準備することが
     推奨されている。
  • 処理の流れ
    処理の流れを以下に示す(参考文献1、参考文献2)。

    1. 手前ウィンドウのウィンドウハンドルを取得する(main関数)
    2. ウィンドウハンドルからプロセスIDを取得する(main関数)
    3. プロセスIDのプロセスを開く(main関数)
    4. プロセスのモジュールハンドルを列挙する(本関数)
    5. モジュールのフルパスを取得する(本関数)

    ※EnumProcessModulesEx について、詳しくは MSDN の
     リファレンスを見てほしい(参考文献3)

テスト

googletest を用いて EnumModuleNames 関数の単体テスト(C0)を実施した。
NuGet を利用し gmock v1.10.0 パッケージマネージャをインストールした。
(main関数の方は適当やで)

環境

開発環境および動作確認を実施したPCの情報を以下に示す。


開発環境
Visual Studio Community 2015

PC情報
OS 名: Microsoft Windows 8.1
OS バージョン: 6.3.9600 N/A ビルド 9600
システム モデル: dynabook KIRA V73/PS
システムの種類: x64-based PC
物理メモリの合計: 8,103 MB
プロセッサ: Intel64 Family 6 Model 61 Stepping 4 GenuineIntel ~2200 Mhz

なぜ作った

必要を感じたから。

  • 見切れているウィンドウタイトルの全体を知りたい
  • 長いウィンドウタイトルをコピーしたい

稀によくあるシチュエーションである。

感想

単体テストしないと落ち着かない性質になってしまった。
バグを出さないことは重要であり、そのためには多大な労力が必要なんだな
と感じる今日この頃である。

参考文献

  1. ウィンドウハンドルから実行ファイル名を取得する
  2. DLL / モジュールの列挙 (PSAPI)
  3. EnumProcessModulesEx function
  4. Windowsのwprintf関数はUnicodeを出力できない?
  5. Google C++ Testing Frameworkをはじめよう

0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています