SnapCrab_使用中のフォルダー_2021-12-13_22-11-4_No-00
ふと、「このファイルは他のプログラムによって使用されています」とWindowsで表示された時に、どのプロセスがファイルをロックしているのかささっと特定できるツールがあれば便利だなと思い、その実現方法を調査しました。

わざわざ作らなくてもWindows標準付属のリソースモニターを使えば確認できますが、いちいちリソースモニターを開いてファイル名を入力するのが面倒だったのでツールも自作しました。

実現方法

Win32APIのRestart ManagerというAPIで実現できます。

Restart Manager について - Win32 apps | Microsoft Docs

手順としては、こう。

  1. Restart Managerセッションを開始する(RmStartSession)
  2. 調査対象のファイルをセッションに登録する(RmRegisterResources)
  3. そのファイルをロックしているプロセスの一覧を取得する(RmGetList)
  4. 用が済んだらセッションを終了する(RmEndSession)

Restart Managerは、更新プログラムを実行する際に更新しなければならないファイルを開いているプロセスを特定し、そのプロセスを再起動させるためのAPIですが、再起動はさせなくともファイルをロックしているプロセスを特定するだけの目的でも利用できます。
ちなみに同様の機能を提供するIFileIsInUseというAPIも存在しますが、こちらはまだ試していません。使い勝手はどちらもほぼ同じだと思います。

注意点

Restart Managerでは比較的簡単にファイルをロックしているプロセスを特定できますが、注意すべきはファイルをロックしているプロセス”しか”特定できないということです。ファイルを何らかのプロセスが開いているとき、以下の2通りの状態が考えられます。

  1. ファイルがロックされている
  2. ファイルはロックされていないが、何らかのプロセスがファイルを開いている

前者はRestart Managerで特定可能です。WordファイルをMS Wordで開くと、ファイルがロックされRestart ManagerでもWordがロックしていることを特定できます。ファイルがロックされているとき、ファイルを削除したり移動したり名称変更したりすることはできません。実は、ファイルがロックされている場合はエクスプローラーでも「何がファイルをロックしているか」が表示されます。
SnapCrab_使用中のファイル_2021-12-13_22-14-26_No-00

一方で、後者はRestart Managerで特定できません。ファイルがロックされていないのでファイルを削除、移動、名称変更することは可能ですが、そのファイルが存在するフォルダを移動したり名称変更しようとすると「別のプログラムがこのフォルダーまたはファイルを開いているので、操作を完了できません」と表示されます。
SnapCrab_使用中のフォルダー_2021-12-13_22-11-4_No-00
これではどのプロセスが原因なのかわかりません。本当はこっちを特定できるようにしたかったのですが、Win32APIのドキュメント化されていない関数を使わなければならず難しいというのが正直なところです。これに関しては色々試しているところで、実現できたらまた記事に書きます。

というわけで、今回実現できるのは前者の「ファイルがロックされている」場合のみです。

CUIで動かす

まずはコマンドプロンプト上で動くものを作ります。型変換周りが面倒だったのでファイルパスはソースコードに直書き(変数lpcwstrで指定)です。

#include <Windows.h>
#include <fileapi.h>
#include <string>

#include <winternl.h>
#include <ntstatus.h>

#include <RestartManager.h>
#include <winerror.h>

#include <stdexcept>
#include <iostream>
#include <cstdlib>

#include <wchar.h>
#include <Shlwapi.h>

#include <locale.h>

#pragma comment(lib, "Rstrtmgr.lib")	// Restart Manager用
#pragma comment(lib, "Shlwapi.lib")		// ファイルの存在確認用

int main() {
	setlocale(LC_CTYPE, "ja_JP.UTF-8");

	DWORD dw_session, dw_error;
	WCHAR sz_session_key[CCH_RM_SESSION_KEY + 1]{};

	dw_error = RmStartSession(&dw_session, 0, sz_session_key);
	if (dw_error != ERROR_SUCCESS) {
		throw std::runtime_error("fail to start restart manager.");
	}

	LPCWSTR lpcwstr = L"D:\\test.docx";			// ここでファイルパスを指定
	printf("Target file : %ls\n", lpcwstr);

	// ファイルの存在をチェック
	if (!PathFileExists(lpcwstr)) {
		printf("The file doesn't exit.\n");
		return 0;
	}
	
	const int files_n = 1;
	dw_error = RmRegisterResources(dw_session, files_n, &lpcwstr, 0, NULL, 0, NULL);
	if (dw_error != ERROR_SUCCESS) {
		printf("fail to register target files.");
	}

	// そのファイルを使用しているプロセスの一覧を取得
	UINT n_proc_info_needed = 0;
	UINT n_proc_info = 256;
	RM_PROCESS_INFO* rgpi = new RM_PROCESS_INFO[n_proc_info];
	DWORD dw_reason;

	dw_error = RmGetList(dw_session, &n_proc_info_needed, &n_proc_info, rgpi, &dw_reason);

	if (dw_error == ERROR_MORE_DATA) {
		delete[] rgpi;
		n_proc_info = n_proc_info_needed;
		rgpi = new RM_PROCESS_INFO[n_proc_info];
		dw_error = RmGetList(dw_session, &n_proc_info_needed, &n_proc_info, rgpi, &dw_reason);
	}
	else if (dw_error == ERROR_SUCCESS) {
		printf("this file is opened by %d processes.\n", (int)n_proc_info_needed);
	}
	else {
		printf("Couldn't get process list: %d\n", dw_error);
	}

	RmEndSession(dw_session);

	for (int i = 0; i < n_proc_info_needed; i++) {
		printf("Process ID:\t\t%d\n", rgpi[i].Process.dwProcessId);
		printf("Application Name:\t%ls\n", rgpi[i].strAppName);
		printf("Application Type:\t%d\n", rgpi[i].ApplicationType);
	}
}

実行結果は以下の通り。
ファイルをWordで開いていないとき:
SnapCrab_NoName_2021-12-29_14-5-40_No-00
ファイルをWordで開いているとき:
SnapCrab_NoName_2021-12-29_14-5-28_No-00

Wordによるファイルロックを検知できている様子が確認できます。

GUIで動かす

ドラッグアンドドロップで動くようにしたかったので、GUIで実装しました。ライブラリにOpenSiv3Dを利用しています。

#include <Siv3D.hpp> // OpenSiv3D v0.6.3
#include <Siv3D/Windows/Windows.hpp>
#include <string>

#include <winternl.h>
#include <ntstatus.h>

#include <RestartManager.h>
#include <winerror.h>

#pragma comment(lib, "Rstrtmgr.lib")

void Main()
{
	// 背景の色を設定 | Set background color
	Scene::SetBackground(ColorF{ 0.3, 0.3, 0.3 });

	Print << U"Drop File Here!";

	while (System::Update())
	{
		// ファイルがドラッグアンドドロップされたら
		if (DragDrop::HasNewFilePaths()) {
			// 文字をクリア
			ClearPrint();

			// ファイルの総数(1つ)
			DroppedFilePath file = DragDrop::GetDroppedFilePaths()[0];
			int files_n = 1;

			// RestartManagerセッションの開始
			DWORD dw_session, dw_error;
			WCHAR sz_session_key[CCH_RM_SESSION_KEY + 1]{};

			dw_error = RmStartSession(&dw_session, 0, sz_session_key);
			if (dw_error != ERROR_SUCCESS) {
				throw std::runtime_error("fail to start restart manager.");
			}

			// ファイルをセッションに登録
			std::wstring wstr = file.path.toWstr();
			PCWSTR pcwstr = wstr.c_str();
			Print << Unicode::FromWstring(pcwstr);

			dw_error = RmRegisterResources(dw_session, files_n, &pcwstr, 0, NULL, 0, NULL);
			if (dw_error != ERROR_SUCCESS) {
				Console << U"Err";
				throw std::runtime_error("fail to register target files.");
			}

			// そのファイルを使用しているプロセスの一覧を取得
			UINT n_proc_info_needed = 0;
			UINT n_proc_info = 1;
			RM_PROCESS_INFO* rgpi = new RM_PROCESS_INFO[n_proc_info];
			DWORD dw_reason;

			dw_error = RmGetList(dw_session, &n_proc_info_needed, &n_proc_info, rgpi, &dw_reason);

			if (dw_error == ERROR_MORE_DATA) {		// rgpiの足りない分を追加
				delete[] rgpi;
				n_proc_info = n_proc_info_needed;
				rgpi = new RM_PROCESS_INFO[n_proc_info];
				dw_error = RmGetList(dw_session, &n_proc_info_needed, &n_proc_info, rgpi, &dw_reason);	// もう一度取得
				Print << U"this file is opened by " << (int)n_proc_info_needed << U" processes.";
			}
			else if (dw_error == ERROR_SUCCESS) {
				Print << U"this file is opened by " << (int)n_proc_info_needed << U" processes.";
			}
			else {
				Print << U"Error";
				break;
			}
			if (dw_error != ERROR_SUCCESS) {
				throw std::runtime_error("fail to get process list.");
			}

			for (int i = 0; i < n_proc_info_needed; i++) {
				Print << U"---------------------------------------------------------------";
				Print << U"プロセスID: " << rgpi[i].Process.dwProcessId;
				Print << U"アプリ名: " << Unicode::FromWstring((std::wstring)rgpi[i].strAppName);
				Print << U"アプリのタイプ: " << rgpi[i].ApplicationType;
				Print << U"---------------------------------------------------------------";
			}

			// セッション終了
			RmEndSession(dw_session);
		}
	}
}

動作確認として、

  1. まだ開いていないWordファイルをドラッグ&ドロップ
  2. そのファイルをWordで開く
  3. もう一度同じWordファイルをドラッグ&ドロップ

の順序で試してみました。

とりあえずWordが特定できてるっぽいので、Restart Managerの利用法としては成功。
ただし、前述の通りTerapadのようにファイルをロックしないプロセスは特定できません。
SnapCrab_NoName_2021-12-13_23-56-5_No-00
次はファイルをロックしないプロセスも特定できるようにしたいなぁ。

リポジトリ

以上のコードが置いてあるGitHubリポジトリはこちら。
https://github.com/YotioSoft/RmWhichProcessLocking