2014/05/09

ファイルを使用中のアプリをC#から調べる

あるファイルを使用中のアプリ(プロセス)を知りたい、少なくともその有無だけでも知りたい、という場面が時々あるが、.NET Frameworkにはこれを簡単に調べる方法が用意されていない。よって、間接的に対応する方法が色々編み出されてきたが、直接的に調べるAPIも(P/Invokeにはなるが)存在する。

目的を果たせれば方法は何でもいいが、直接、ストレートにできる方法があるならその方がいいと思う。具体的にはRestart Manager APIを利用する方法だが、これはVista以降でのみ使用可。したがって従来なら少し二の足を踏むところだが、晴れてXPのことは考えなくてよくなったので(一応)、これも一つの福音と言えよう。

1. ラッパークラス

具体的な方法は既に説明されていて、必要なことは全部書いてある。
  • MSDN Magazine 2007 April: 再起動マネージャとジェネリックメソッドのコンパイル
これをもう少し使いやすい形にしてみる。

処理としては、大まかに以下の4段階。
  1. Restart Managerセッションを開始する(RmStartSession
  2. 対象のファイルをセッションに登録する(RmRegisterResource
  3. 対象のファイルを使用中のプロセスをセッションから取得する(RmGetList
  4. セッションを終了する(RmEndSession
これを行うラッパークラスは以下のとおり。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;

/// <summary>
/// A partial wrapper class for Restart Manager API
/// </summary>
/// <remarks>
/// This class is based on http://msdn.microsoft.com/en-us/magazine/cc163450.aspx
/// To use this class, OS has to be Windows Vista or newer.
/// </remarks>
public static class RestartManager
{
  #region "Win32"

  // Start a Restart Manager session.
  [DllImport("Rstrtmgr.dll", CharSet = CharSet.Unicode)]
  private static extern uint RmStartSession(
    out uint pSessionHandle,
    uint dwSessionFlags,
    string strSessionKey);

  // End a Restart Manager session.
  [DllImport("Rstrtmgr.dll")]
  private static extern uint RmEndSession(
    uint dwSessionHandle);

  // Register target files to a Restart Manager session.
  [DllImport("Rstrtmgr.dll", CharSet = CharSet.Unicode)]
  private static extern uint RmRegisterResources(
    uint dwSessionHandle,
    uint nFiles,
    string[] rgsFilenames,
    uint nApplications,
    RM_UNIQUE_PROCESS[] rgApplications,
    uint nServices,
    string[] rgsServiceNames);

  // Get processes using target files with a Restart Manager session.
  [DllImport("Rstrtmgr.dll")]
  private static extern uint RmGetList(
    uint dwSessionHandle,
    out uint pnProcInfoNeeded,
    ref uint pnProcInfo,
    [In, Out] RM_PROCESS_INFO[] rgAffectedApps,
    out uint lpdwRebootReasons);
  
  [StructLayout(LayoutKind.Sequential)]
  private struct RM_UNIQUE_PROCESS
  {
    public uint dwProcessId;
    public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
  }

  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  private struct RM_PROCESS_INFO
  {
    public RM_UNIQUE_PROCESS Process;

    // CCH_RM_MAX_APP_NAME + 1 = 256
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public string strAppName;

    // CCH_RM_MAX_SVC_NAME + 1 = 64
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
    public string strServiceShortName;

    public RM_APP_TYPE ApplicationType;
    public uint AppStatus;
    public uint TSSessionId;

    [MarshalAs(UnmanagedType.Bool)]
    public bool bRestartable;
  }

  private enum RM_APP_TYPE
  {
    /// <summary>
    /// The application cannot be classified as any other type.
    /// </summary>
    RmUnknownApp = 0,

    /// <summary>
    /// A Windows application run as a stand-alone process that displays a top-level window.
    /// </summary>
    RmMainWindow = 1,

    /// <summary>
    /// A Windows application that does not run as a stand-alone process and does not display a top-level window.
    /// </summary>
    RmOtherWindow = 2,

    /// <summary>
    /// The application is a Windows service.
    /// </summary>
    RmService = 3,

    /// <summary>
    /// The application is Windows Explorer.
    /// </summary>
    RmExplorer = 4,

    /// <summary>
    /// The application is a stand-alone console application.
    /// </summary>
    RmConsole = 5,

    /// <summary>
    /// The process may be a critical process and cannot be shut down.
    /// </summary>
    RmCritical = 1000
  }

  private const uint ERROR_SUCCESS = 0;
  private const uint ERROR_MORE_DATA = 234;

  #endregion

  /// <summary>
  /// Check if any process is using a specified file.
  /// </summary>
  /// <param name="filePath">Path of target file</param>
  /// <returns>True if using</returns>
  public static bool IsProcessesUsingFile(string filePath)
  {
    bool isUsing = false;
    foreach (var proc in EnumerateProcessesUsingFiles(new[] { filePath }))
    {
      if (proc == null)
        continue;

      isUsing = true;
      proc.Dispose();
    }

    return isUsing;
  }

  /// <summary>
  /// Enumerate processes using specified files.
  /// </summary>
  /// <param name="filePaths">Paths of target files</param>
  /// <returns>Processes using target files</returns>
  /// <remarks>Caller is responsible for disposing the processes.</remarks>
  public static IEnumerable<Process> EnumerateProcessesUsingFiles(param string[] filePaths)
  {
    if ((filePaths == null) || !filePaths.Any())
      yield break;

    if (!OsVersion.IsVistaOrNewer)
      yield break;

    uint sessionHandle = 0; // Handle to Restart Manager session

    try
    {
      // Start a Restart Manager session.
      var result1 = RmStartSession(
        out sessionHandle,
        0,
        Guid.NewGuid().ToString("N"));

      if (result1 != ERROR_SUCCESS)
        throw new Win32Exception("Failed to start a Restart Manager session.");

      // Register target files to the session.
      var result2 = RmRegisterResources(
        sessionHandle,
        (uint)filePaths.Length,
        filePaths,
        0U,
        null,
        0U,
        null);

      if (result2 != ERROR_SUCCESS)
        throw new Win32Exception("Failed to register target files to a Restart Manager session.");

      // Get processes using target files with the session.
      uint pnProcInfoNeeded = 0;
      uint pnProcInfo = 0;
      RM_PROCESS_INFO[] rgAffectedApps = null;
      uint lpdwRebootReasons;

      uint result3 = 0;

      do
      {
        result3 = RmGetList(
          sessionHandle,
          out pnProcInfoNeeded,
          ref pnProcInfo,
          rgAffectedApps,
          out lpdwRebootReasons);

        switch (result3)
        {
          case ERROR_SUCCESS: // The size of RM_PROCESS_INFO array is appropriate.
            if (pnProcInfo == 0)
              break;

            // Yield the processes.
            foreach (var app in rgAffectedApps)
            {
              Process proc = null;
              try
              {
                proc = Process.GetProcessById((int)app.Process.dwProcessId);
              }
              catch (ArgumentException)
              {
                // None (In case the process is no longer running).
              }

              if (proc != null)
                yield return proc;
            }
            break;

          case ERROR_MORE_DATA: // The size of RM_PROCESS_INFO array is not enough.
            // Set RM_PROCESS_INFO array to store the processes.
            rgAffectedApps = new RM_PROCESS_INFO[(int)pnProcInfoNeeded];
            pnProcInfo = (uint)rgAffectedApps.Length;
            break;

          default:
            throw new Win32Exception("Failed to get processes using target files with a Restart Manager session.");
        }
      }
      while (result3 != ERROR_SUCCESS);
    }
    finally
    {
      // End the session.
      RmEndSession(sessionHandle);
    }
  }
}
EnumerateProcessesUsingFilesメソッド中の3段階目のRmGetListがポイントで、使用中のプロセスの情報を格納するためのRM_PROCESS_INFO構造体の配列の長さをどう決めるかが問題になる。初回は183と184行のように配列の長さを0、配列をnullとしてRmGetListを実行すると、
  • 使用中のプロセスが存在しない場合、配列はそのままでよく、ERROR_SUCCESSが返ってくるので、そのまま抜ける。
  • 使用中のプロセスが存在する場合、配列の長さが足りず、ERROR_MORE_DATAが返ってくるので、224と225行のように配列を整え直した上でループさせ、再度RmGetListを実行する。
    • それで問題なければ、ERROR_SUCCESSが返ってくるので、プロセスをyield returnで返す。もしこの間に終了しているプロセスがあればArgumentExceptionが出るが、それはtry-catchで潰す。
    • もしプロセスが増えていれば、同じことの繰り返し。
という流れになる。

このクラスを含むサンプルアプリ

[修正]

IsProcessesUsingFileメソッドの中でプロセスをDisposeするように修正し、合わせてEnumerateProcessesUsingFilesメソッドをIEnumerable<Process>を返すように変更した。

2. 注意点

返ってきたList<Process>を使って如何ようにも処理は可能だが、注意点としてこのメソッドの前後の短い間にもプロセスの状況は変わる可能性があるので、例えばファイルに書き込む前の事前チェックに使う場合でもtry-catchで例外に備えるべき必要性は変わらない。

0 コメント :