суббота, 18 февраля 2012 г.

Как мне найти программу, которая держит этот файл?

Это перевод How do I find out which process has a file open? Автор: Реймонд Чен.

Исторически, нет никакого специального способа найти процесс, который держит файл. Файловый объект имеет обычный счётчик ссылок объекта ядра и когда счётчик опускается до нуля - файл закрывается. Но в системе никто не отслеживает процессы, открывшие данный описатель, и сколько именно раз они его открыли (и это упрощённое изложение даже игнорирует тот факт, что счётчик может быть увеличен вовсе не процессом, а, скажем, драйвером режима ядра; или, быть может, изначально счётчик был увеличен процессом, который теперь уже закрыт, но файл ещё держится драйвером ядра).

Это состояние вещей согласуется с концепцией не хранить информацию, которая вам не нужна. Файловую систему не заботит, кто там держит её файлы. Её задача - закрыть файл, когда уйдёт последняя ссылка.

Аналогичную ситуацию вы видите в COM. Всё, что вас заботит - когда же счётчик опустится до нуля (потому что в этот момент вам нужно удалить объект). Если позже вы обнаружите в своём процессе утечку, то у вас нет никакого волшебного запроса "Покажи мне всех, то вызывал _AddRef для моего объекта" - просто потому, что вы никогда и не вели учёт вызывающих _AddRef. Нет у вас и возможности сделать так: "Вот объект, который я хочу удалить. Покажи мне всех использующих его, так что я смогу их удалить".

По крайней мере таким был классический сценарий.

А теперь познакомьтесь с Restart Manager.

Официальная цель Restart Manager - оказать помощь в закрытии и перезапуске приложений, которые вы хотите обновить. Чтобы сделать это, вам нужно отслеживать, какие процессы держат ссылки и на какие файлы. И вот она та самая база данных, что нам нужна (почему это ядро хранит список процессов, открывших файл? Потому что это принцип, обратный к принципу не хранить вещи, которые вам не нужны: теперь ядру нужна эта информация!)

Вот простая программа, которая принимает в командной строке имя файла и показывает список процессов, открывших этот файл.
program Project78;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Winapi.Windows,
  System.SysUtils;

{$A+,Z4}

const
  PROCESS_QUERY_LIMITED_INFORMATION = $1000;

  RstrtMgr = 'Rstrtmgr.dll';

  RM_SESSION_KEY_LEN = SizeOf(TGUID);          // RM_SESSION_KEY_LEN - size in bytes of binary session key
  CCH_RM_SESSION_KEY = RM_SESSION_KEY_LEN * 2; // CCH_RM_SESSION_KEY - character count of text-encoded session key
  CCH_RM_MAX_APP_NAME = 255;                   // CCH_RM_MAX_APP_NAME - maximum character count of application friendly name
  CCH_RM_MAX_SVC_NAME = 63;                    // CCH_RM_MAX_SVC_NAME - maximum character count of service short name
  RM_INVALID_TS_SESSION = -1;                  // Uninitialized value for TS Session ID
  RM_INVALID_PROCESS = -1;                     // Uninitialized value for Process ID

type
  TAppName = array[0..CCH_RM_MAX_APP_NAME] of WideChar;
  TServiceName = array[0..CCH_RM_MAX_SVC_NAME] of WideChar;
  TSessionKey = array[0..CCH_RM_SESSION_KEY] of WideChar;

  _RM_APP_TYPE = (
    RmUnknownApp = 0,   // Application type cannot be classified in known categories
    RmMainWindow = 1,   // Application is a windows application that displays a top-level window
    RmOtherWindow = 2,  // Application is a windows app but does not display a top-level window
    RmService = 3,      // Application is an NT service
    RmExplorer = 4,     // Application is Explorer
    RmConsole = 5,      // Application is Console application
    RmCritical = 1000   // Application is critical system process where a reboot is required to restart
  );
  RM_APP_TYPE = _RM_APP_TYPE;
  TRMAppType = RM_APP_TYPE;

  _RM_SHUTDOWN_TYPE = (
    RmForceShutdown = $1,          // Force app shutdown
    RmShutdownOnlyRegistered = $10 // Only shudown apps if all apps registered for restart
  );
  RM_SHUTDOWN_TYPE = _RM_SHUTDOWN_TYPE;
  TRMShutdownType = RM_SHUTDOWN_TYPE;

  _RM_APP_STATUS = (
    RmStatusUnknown = $0,          // Application in unknown state or state not important
    RmStatusRunning = $1,          // Application is currently running
    RmStatusStopped = $2,          // Application stopped by Restart Manager
    RmStatusStoppedOther = $4,     // Application detected stopped by outside action
    RmStatusRestarted = $8,        // Application restarted by Restart Manager
    RmStatusErrorOnStop = $10,     // An error occurred when stopping this application
    RmStatusErrorOnRestart = $20,  // An error occurred when restarting this application
    RmStatusShutdownMasked = $40,  // Shutdown action masked by filer
    RmStatusRestartMasked = $80    // Restart action masked by filter
  );
  RM_APP_STATUS = _RM_APP_STATUS;
  TRMAppStatus = RM_APP_STATUS;

  _RM_REBOOT_REASON = (
    RmRebootReasonNone = $0,               // Reboot not required
    RmRebootReasonPermissionDenied = $1,   // Current user does not have permission to shut down one or more detected processes
    RmRebootReasonSessionMismatch = $2,    // One or more processes are running in another TS session.
    RmRebootReasonCriticalProcess = $4,    // A critical process has been detected
    RmRebootReasonCriticalService = $8,    // A critical service has been detected
    RmRebootReasonDetectedSelf = $10       // The current process has been detected
  );
  RM_REBOOT_REASON = _RM_REBOOT_REASON;
  TRMRebootReason = RM_REBOOT_REASON;

  _RM_UNIQUE_PROCESS = record
    dwProcessId: DWORD;               // PID
    ProcessStartTime: TFileTime;      // Process creation time
  end;
  RM_UNIQUE_PROCESS = _RM_UNIQUE_PROCESS;
  PRM_UNIQUE_PROCESS = ^_RM_UNIQUE_PROCESS;
  TRMUniqueProcess = RM_UNIQUE_PROCESS;
  PRMUniqueProcess = PRM_UNIQUE_PROCESS;

  _RM_PROCESS_INFO = record
    Process: TRMUniqueProcess;         // Unique process identification
    strAppName: TAppName;              // Application friendly name
    strServiceShortName: TServiceName; // Service short name, if applicable
    ApplicationType: TRMAppType;       // Application type
    AppStatus: ULONG;                  // Bit mask of application status
    TSSessionId: DWORD;                // Terminal Service session ID of process (-1 if n/a)
    bRestartable: BOOL;                // Is application restartable?
  end;
  RM_PROCESS_INFO = _RM_PROCESS_INFO;
  PRM_PROCESS_INFO = ^_RM_PROCESS_INFO;
  TRMProcessInfo = RM_PROCESS_INFO;
  PRMProcessInfo = PRM_PROCESS_INFO;

function QueryFullProcessImageName(hProcess: THandle; dwFlags: DWORD; lpExeName: PChar; var lpdwSize: Integer): BOOL; stdcall; external kernel32 name {$IFDEF UNICODE}'QueryFullProcessImageNameW'{$ELSE}'QueryFullProcessImageNameA'{$ENDIF};

function RmStartSession(out pSessionHandle: DWORD; dwSessionFlags: DWORD; out strSessionKey: TSessionKey): DWORD; stdcall; external RstrtMgr;
function RmEndSession(dwSessionHandle: DWORD): DWORD; stdcall; external RstrtMgr;
function RmRegisterResources(dwSessionHandle: DWORD; nFiles: UINT; rgsFileNames: PPWideChar; nApplications: UINT; rgApplications: PRMUniqueProcess; nServices: UINT; rgsServiceNames: PPWideChar): DWORD; stdcall; external RstrtMgr;
function RmGetList(dwSessionHandle: DWORD; out pnProcInfoNeeded: UINT; var pnProcInfo: UINT; out rgAffectedApps: TRMProcessInfo; out lpdwRebootReasons: DWORD): DWORD; stdcall; external RstrtMgr;

procedure Run;

  function StrFromAppType(const AAppType: TRMAppType): String;
  begin
    case AAppType of
      RmMainWindow:  Result := 'Application is a windows application that displays a top-level window';
      RmOtherWindow: Result := 'Application is a windows app but does not display a top-level window';
      RmService:     Result := 'Application is an NT service';
      RmExplorer:    Result := 'Application is Explorer';
      RmConsole:     Result := 'Application is Console application';
      RmCritical:    Result := 'Application is critical system process where a reboot is required to restart';
    else
                     Result := 'Application type cannot be classified in known categories';
    end;
  end;

const
  Num = 10;
var
  dwSession: DWORD;
  szSessionKey: TSessionKey;
  pszFile: PPWideChar;
  P: PWideChar;
  FileName: WideString;
  dwReason: DWORD;
  i: Integer;
  nProcInfoNeeded: UINT;
  nProcInfo: UINT;
  rgpi: array[0..Num - 1] of TRMProcessInfo;
  hProcess: THandle;
  ftCreate, ftExit, ftKernel, ftUser: TFileTime;
  sz: String;
  cch: Integer;
begin
  FileName := ParamStr(1);

  FillChar(szSessionKey, SizeOf(szSessionKey), 0);
  SetLastError(RmStartSession(dwSession, 0, szSessionKey));
  Win32Check(GetLastError = ERROR_SUCCESS);
  try
    P := PWideChar(FileName);
    pszFile := @P;
    SetLastError(RmRegisterResources(dwSession, 1, pszFile, 0, nil, 0, nil));
    Win32Check(GetLastError = ERROR_SUCCESS);

    nProcInfo := Num;
    SetLastError(RmGetList(dwSession, nProcInfoNeeded, nProcInfo, rgpi[0], dwReason));
    Win32Check(GetLastError = ERROR_SUCCESS);

    for i := 0 to nProcInfo - 1 do
    begin
      WriteLn(Format('%d.ApplicationType = %d (%s)', [i, Ord(rgpi[i].ApplicationType), StrFromAppType(rgpi[i].ApplicationType)]));
      WriteLn(Format('%d.strAppName = %s', [i, rgpi[i].strAppName]));
      WriteLn(Format('%d.Process.dwProcessId = %d', [i, rgpi[i].Process.dwProcessId]));

      hProcess := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, rgpi[i].Process.dwProcessId);
      if hProcess <> 0 then
      try
        if GetProcessTimes(hProcess, ftCreate, ftExit, ftKernel, ftUser) and
           (CompareFileTime(rgpi[i].Process.ProcessStartTime, ftCreate) = 0) then
        begin
          cch := MAX_PATH;
          SetLength(sz, cch);
          if QueryFullProcessImageName(hProcess, 0, PChar(sz), cch) and
             (cch <= MAX_PATH) then
          begin
            SetLength(sz, cch);
            WriteLn(Format('%d.Process.Name = %s', [i, sz]));
          end;
        end;
      finally
        CloseHandle(hProcess);
      end;
      WriteLn;
    end;
  finally
    RmEndSession(dwSession);
  end;
end;

begin
  try
    if ParamCount = 0 then
      Exit;
    Run;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Итак, первой строкой в этом коде... нет, постойте - ещё до вызова функции Rm­Start­Session у нас есть строка
FillChar(szSessionKey, SizeOf(szSessionKey), 0);
Одна эта строка кода решает аж два бага!

Первый из них - баг в документации. Документация по функции Rm­Start­Session не указывает, насколько большим должен быть буфер для ключа сессии. Правильный ответ - CCH_RM_SESSION_KEY + 1.

Второй баг - в коде. Функция Rm­Start­Session не завершает ключ терминатором, даже хотя прототип функции описан как возвращающий нуль-терминированную строку. Чтобы обойти эту проблему, мы очищаем буфер перед использованием, заполняя его нулями, так что то, что будет записано в ключ сессии, автоматически получит корректный терминатор (а именно - один из тех нулей, что мы записали).

Прим. пер.: при переводе с C на Delphi прототип функции был существенно изменён. Так что теперь ключ сессии передаётся как фиксированный массив.

Окей, эти проблемы ушли с дороги. Теперь, базовый алгоритм:
  1. Создать сессию Restart Manager.
  2. Добавить интересующий нас файл в сессию.
  3. Запросить список процессов, влияющих на ресурс.
  4. Напечатать немного информации по каждому процессу.
  5. Закрыть сессию.
Я уже упомянул, что вы создаёте сессию вызовом Rm­Start­Session. Следующим шагом мы добавляем в сессию единственный файл вызовом Rm­Register­Resources.

Теперь начинается веселье. Получение списка процессов обычно происходит в два этапа. Сначала вы запрашиваете число доступных процессов (передавая 0 в nProcInfo), затем выделяете память и вызываете функцию второй раз для получения данных. Но поскольку это просто пример, я вшил в программу фиксированное число процессов. Если файл открыт более 10 процессами, то я просто сдамся (вы можете проверить это, запустив программу и указав файл вроде kernel32.dll).

Прим.пер.: и если какой-то сильно умный читатель вздумает скопировать этот код и использовать "как есть"...

Вторая хитрая часть заключается в поиске процесса по TRMProcessInfo. Поскольку ID процессов могут использоваться заново (recycle), то структура TRMProcessInfo идентифицирует процесс комбинацией ID и времени его запуска. Такая комбинация является уникальной в рамках одной машины, потому что два процесса не могут иметь одинаковые ID в одно и то же время. Поэтому мы открываем процесс по его ID, а затем проверяем, что это именно тот процесс, что нам нужен (а если нет - то ID ссылается на процесс, который уже завершил работу с того момента, когда мы запрашивали список). Если же все данные совпали, то мы выводим путь к .exe файлу процесса.

Вот и всё, что нужно, чтобы перечислить все процессы, открывшие какой-то конкретный файл.

Конечно же, более выразительным интерфейсом для управления используемыми файлами является IFileIsInUse, который я упоминал не так давно. Этот интерфейс скажет вам не только какие приложения открыли файл (и в более дружелюбном формате, чем просто путь к .exe), но вы также сможете переключиться на это приложение и даже попросить его закрыть файл (если оно поддерживает эту возможность). Сама Windows 7 сначала пытается использовать IFileIsInUse, и лишь если он не сумел освободить файл - обращается к Restart Manager.

Читать далее: использование IFileIsInUse.

4 комментария:

  1. Где-то я видел подобное описание. Даже слово в слово. Только на английском и с++. Вот же оно: http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx

    ОтветитьУдалить
  2. Ээээ... слова "это перевод... автор - Реймонд Чен" ни о чём не говорят? :)

    ОтветитьУдалить
  3. Ну хоть что-то более менее нормальное появилось да еще и документированное.
    А раньше приходилось ручками все делать :)
    http://rouse.drkb.ru/winapi.php#enumopenfiles

    ОтветитьУдалить
  4. FillChar(szSessionKey, SizeOf(szSessionKey), 0);

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.