На форумах 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
. Ну, это действительно приятная возможность. Но не доверяйте ей слепо! Всегда перепроверьте блокировку файла после вызова прежде чем двигаться дальше.
Следующий шаг - узнать, где размещаются заблокированные файлы. Это место - 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
См. также: Как мне найти программу, которая держит этот файл?
Более короткий путь к проверке использования/не использования файла с применением данной технологии может быть следующим:
ОтветитьУдалить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;
А смысл в такой функции? Проще просто открыть файл. Успех - ОК, провал - файл занят.
УдалитьА смысл заметки - в получении имени приложения.
Прошу прощения
ОтветитьУдалить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;
Хоть 90% процентов кода и гуано http://www.gunsmoker.ru/2010/05/90.html, но этот пример реально помог. Спасибо.
ОтветитьУдалить