воскресенье, 19 февраля 2012 г.

Используется ли файл?

Это перевод Is File In Use. Автор: Christian Wimmer.

На форумах DelphiPraxis задали вопрос, который обычно всплывает несколько раз в год. Однако в этот раз я мог сказать, что появился API, который решает эту проблему. Вопрос был о том, как определить, кто держит файл. Главной проблемой стало то, что я никак не мог вспомнить название API, так что Assarbad пришлось постараться и поискать его.

И это имя - IFileIsInUse, интерфейс. Он объявлен в Shobjidl.h, Shobjidl.idl и, сейчас уже, в JwaShlObj.pas. Вот вырезка кода:
const
  IID_IFileIsInUse: TGUID = (
    D1:$64a1cbf0; D2:$3a1a; D3:$4461; D4:($91,$58,$37,$69,$69,$69,$39,$50));

type
  {$ALIGN 4}
  tagFILE_USAGE_TYPE = (
    FUT_PLAYING = 0,
    FUT_EDITING = 1,
    FUT_GENERIC = 2
  );
  FILE_USAGE_TYPE = tagFILE_USAGE_TYPE;
  TFileUsageType = FILE_USAGE_TYPE;

const
  OF_CAP_CANSWITCHTO     = $0001;
  OF_CAP_CANCLOSE        = $0002;

type
  IFileIsInUse = interface(IUnknown)
    ['{64a1cbf0-3a1a-4461-9158-376969693950}']
    function GetAppName(out ppszName: LPWSTR) : HRESULT; stdcall;
    function GetUsage(out pfut : FILE_USAGE_TYPE) : HRESULT; stdcall;
    function GetCapabilities(out pdwCapFlags : DWORD) : HRESULT; stdcall;
    function GetSwitchToHWND(out phwnd : HWND) : HRESULT; stdcall;
    function CloseFile() : HRESULT; stdcall;
  end;
Интерфейс может использоваться двояко - и клиентом и быть реализованным сервером. Клиент обычно проверяет, заблокирован ли файл, и получает ссылку на интерфейс для вызова его методов. Сервер же может держать блокировку на файле и реализовывать этот интерфейс, чтобы предоставить его услуги клиенту. Эта статья коснётся только стороны клиента.

Вы увидите, что этот API будет работать только если обе стороны выполняют свою работу. Процесс, блокирующий файл, также должен реализовать этот интерфейс и зарегистрировать его, чтобы клиент мог получать информацию. Нет никакой прямой связи между блокировкой файла и именем процесса. Если процесс держит блокировку на файл, но не реализует интерфейс, то вы снова оказываетесь сами за себя.

В конечном итоге, это просто помощник Оболочки для красивого диалога удаления файлов Windows:


Итак, напомню вид самого интерфейса:
IFileIsInUse = interface(IUnknown)
  function GetAppName(out ppszName: LPWSTR) : HRESULT; stdcall;
  function GetUsage(out pfut : FILE_USAGE_TYPE) : HRESULT; stdcall;
  function GetCapabilities(out pdwCapFlags : DWORD) : HRESULT; stdcall;
  function GetSwitchToHWND(out phwnd : HWND) : HRESULT; stdcall;
  function CloseFile() : HRESULT; stdcall;
end;
Методы интерфейса достаточно просто понять:
  • GetAppName получает имя процесса, который держит файл. Это будет произвольное имя, выбранное самим приложением. Всегда используйте PWideChar для получения имени и не забывайте освободить память с помощью CoTaskMemFree (или через IMalloc). Не забывайте проверять результат вызова. Если вам нравятся исключения, то вы можете изменить прототип метода на safecall.

     
  • GetUsage возвращает причину блокировки файла. Это может быть одна из констант в перечислении TFileUsage. Это может быть проигрывание видео, музыки или редактирование файла. Либо же это может быть общая причина FUT_GENERIC. Возможно, в будущем будет добавлено больше кодов.
     
  • GetCapabilities возвращает набор флагов. Они указывают, можно ли попросить закрыть файл вызовом CloseFile (OF_CAP_CANCLOSE) или же мы можем активировать приложение, блокирующее файл (OF_CAP_CANSWITCHTO). Лучше всего уведомить пользователя о переключении окон, иначе он может сильно расстроиться из-за внезапно пропавшего окна вашего приложения. Само переключение окон можно сделать вызовом SetForegroundWindow. Но для успешности выполнения этой операции ваше приложение должно иметь фокус.
     
  • GetSwitchToHWND возвращает описатель окна процесса, блокирующего файл. Используйте этот описатель только для переключения на окно. Вы понятия не имеете, что за окно вам могут тут вернуть. Не нужно пытаться его закрыть или делать что-то ещё - это просто будет плохим поведением. И всегда проверяйте на ошибки вызова. Иначе вы не узнаете, вернули ли вам корректный описатель.

    Конечно же, вы можете вызывать этот метод только если вызов GetCapabilities вернул бит OF_CAP_CANSWITCHTO.
     
  • CloseFile просит сервер закрыть файл. Вы можете вызывать этот метод только если вызов GetCapabilities вернул бит OF_CAP_CANCLOSE. Ну, это действительно приятная возможность. Но не доверяйте ей слепо! Всегда перепроверьте блокировку файла после вызова прежде чем двигаться дальше.
Чтобы получить интерфейс для файла вам нужно сначала иметь заблокированный файл. Вы можете скачать и скомпилировать пример из MSDN с именем IsFileInUse или вы можете просто открыть файл в приложении, которое реализует такие интерфейсы (например, MS Office).

Следующий шаг - узнать, где размещаются заблокированные файлы. Это место - running object table, или ROT для краткости (её можно получить через ActiveX.GetRunningObjectTable()). Это глобальная* (для машины) таблица, которая хранит запущенные (running) COM объекты. Вы можете реализовать свой собственный интерфейс и поместить его в эту таблицу. Поскольку интерфейсы могут быть произвольными, к ним прикрепляются моникеры (IMoniker), которые уникально их описывают. Существует несколько типов моникеров вроде класса (class), элемента (item) и файла (file) - и нас интересует только последний тип.

Так что вся работа будет заключаться в переборе моникеров в ROT. Обычно их там не много (у меня было всего 5). Каждый моникер проверяется на тип (IsSystemMoniker) и то, является ли он файловым моникером (MKSYS_FILEMONIKER), а затем путь к файлу сравнивается с нашим файлом. Если честно, то я не могу сказать, зачем пример сперва сравнивает префикс, а потом сам моникер - извините.

Сам объект получается по моникеру через вызов GetObject у ROT. Этот вызов может завершиться с ошибкой E_ACCESS_DENIED, так что проверяйте на ошибки. В итоге мы получаем интерфейс IFileIsInUse. Поскольку файл мог быть зарегистрирован без реализации этого интерфейса, то нам снова нужна проверка на ошибки.

Я не писал весь этот код сам. Фактически, я взял за основу уже упоминавшийся мною пример из MSDN.
function GetFileInUseInfo(const FileName : WideString) : IFileIsInUse;
var
  ROT : IRunningObjectTable;
  mFile, enumIndex, Prefix : IMoniker;
  enumMoniker : IEnumMoniker;
  MonikerType : LongInt;
  unkInt  : IInterface;
begin
  result := nil;

  OleCheck(GetRunningObjectTable(0, ROT));
  OleCheck(CreateFileMoniker(PWideChar(FileName), mFile));

  OleCheck(ROT.EnumRunning(enumMoniker));

  while (enumMoniker.Next(1, enumIndex, nil) = S_OK) do
  begin
    OleCheck(enumIndex.IsSystemMoniker(MonikerType));
    if MonikerType = MKSYS_FILEMONIKER then
    begin
      if Succeeded(mFile.CommonPrefixWith(enumIndex, Prefix)) and
         (mFile.IsEqual(Prefix) = S_OK) then
      begin
       if Succeeded(ROT.GetObject(enumIndex, unkInt)) then
        begin
          if Succeeded(unkInt.QueryInterface(IID_IFileIsInUse, result)) then
          begin
            result := unkInt as IFileIsInUse;
            exit;
          end;
        end;
      end;
    end;
  end;
end;

Заключение

В целом этот API имеет такие недостатки:
  • Этот API рассчитывает на то, что программа, заблокировавшая файл, реализует и зарегистрирует специальный интерфейс. Если же приложение не озаботится этой задачей, то вы не сможете использовать этот метод.
  • Если вы заблокируете файл на разделяемой сетевой папке, а затем попробуете обратиться к нему по локальному имени, то не сможете определить процесс, заблокировавший файл. Причина кроется в самой Windows. Windows является провайдером файлов внешнему миру (даже если это всего лишь петля обратно на локальную машину), так что Проводник Windows покажет просто "Система" в качестве блокиратора. Также, в ROT вы увидите только сетевой (UNC) путь вместо локального.
  • (*) Примечание безопасности: ROT не является по настоящему глобальной. Фактически, в системе есть несколько ROT. ROT делятся индивидуально по пользователям, а затем по mandatory integrity control (MIC) или integrity levels (IL) - высокому (high), среднему (medium) и, вероятно, низкому (low) (я не проверял). Если вы запущены как обычный пользователь, ваши процессы будут иметь средний уровень, а процессы администратора - высокий. Программа со средним уровнем сможет зарегистрировать себя только в ROT для среднего уровня. Поэтому, если она хочет создать ключи реестра для COM (LOCAL_MACHINE\Classes\AppID\guid), то ей нужно запуститься под администратором хотя бы раз. Таким образом, она сможет стать видимой для всех ROT при желании. К сожалению, это ещё не всё. На многопользовательской системе каждый объект в моникере имеет дескриптор безопасности, который говорит COM, кто имеет к нему доступ. Если Алиса хочет получить доступ к ROT, созданной Бобом, то Боб должен явно разрешить Алисе доступ к ней. Как обычно в вопросах безопасности, по умолчанию доступ разрешается только системе, администраторам и создателю - эти настройки копируются из глобальных настроек безопасности COM и они могут быть изменены либо в реестре для AppID при запуске процесса, либо для каждого регистрируемого объекта сервером. JWSCL даёт доступ к обоим вариантам в файле JwsclComSecurity.pas (>= 0.9.4). И таким образом сервер может изменить настройки, чтобы, скажем, дать всем прошедшим проверку пользователям доступ к объекту.

Пример

Вы можете скачать файл-пример напрямую с Subversion:
https://jedi-apilib.svn.sourceforge.net/svnroot/jedi-apilib/jwapi/trunk/Examples/FileIsInUse/Client/FileIsInUseClientExample.dpr

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

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

  1. Более короткий путь к проверке использования/не использования файла с применением данной технологии может быть следующим:
    function FileInUse(const AFileName: WideString): boolean;
    var
    AFileMoniker: IMoniker;
    ACTX: IBindCtx;
    AHandleFile: THandle;
    begin
    CreateBindCtx(0,ACTX);
    OleCheck(CreateFileMoniker(PWideChar(AFileName), AFileMoniker));
    if (AFileMoniker.IsRunning(ACTX, nil, nil) = S_OK) then exit(true);
    end;

    ОтветитьУдалить
    Ответы
    1. А смысл в такой функции? Проще просто открыть файл. Успех - ОК, провал - файл занят.

      А смысл заметки - в получении имени приложения.

      Удалить
  2. Прошу прощения

    function FileInUseM(const AFileName: WideString): boolean;
    var
    AFileMoniker: IMoniker;
    ACTX: IBindCtx;
    AHandleFile: THandle;
    begin
    CreateBindCtx(0,ACTX);
    OleCheck(CreateFileMoniker(PWideChar(AFileName), AFileMoniker));
    if (AFileMoniker.IsRunning(ACTX, nil, nil) = S_OK) then exit(true);
    exit(false); // Ж-(
    end;

    ОтветитьУдалить
  3. Хоть 90% процентов кода и гуано http://www.gunsmoker.ru/2010/05/90.html, но этот пример реально помог. Спасибо.

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

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

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

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

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

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