Исторически, нет никакого специального способа найти процесс, который держит файл. Файловый объект имеет обычный счётчик ссылок объекта ядра и когда счётчик опускается до нуля - файл закрывается. Но в системе никто не отслеживает процессы, открывшие данный описатель, и сколько именно раз они его открыли (и это упрощённое изложение даже игнорирует тот факт, что счётчик может быть увеличен вовсе не процессом, а, скажем, драйвером режима ядра; или, быть может, изначально счётчик был увеличен процессом, который теперь уже закрыт, но файл ещё держится драйвером ядра).
Это состояние вещей согласуется с концепцией не хранить информацию, которая вам не нужна. Файловую систему не заботит, кто там держит её файлы. Её задача - закрыть файл, когда уйдёт последняя ссылка.
Аналогичную ситуацию вы видите в 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.
Итак, первой строкой в этом коде... нет, постойте - ещё до вызова функции RmStartSession у нас есть строка
FillChar(szSessionKey, SizeOf(szSessionKey), 0);Одна эта строка кода решает аж два бага!
Первый из них - баг в документации. Документация по функции
RmStartSession не указывает, насколько большим должен быть буфер для ключа сессии. Правильный ответ - CCH_RM_SESSION_KEY + 1.Второй баг - в коде. Функция
RmStartSession не завершает ключ терминатором, даже хотя прототип функции описан как возвращающий нуль-терминированную строку. Чтобы обойти эту проблему, мы очищаем буфер перед использованием, заполняя его нулями, так что то, что будет записано в ключ сессии, автоматически получит корректный терминатор (а именно - один из тех нулей, что мы записали).Прим. пер.: при переводе с C на Delphi прототип функции был существенно изменён. Так что теперь ключ сессии передаётся как фиксированный массив.
Окей, эти проблемы ушли с дороги. Теперь, базовый алгоритм:
- Создать сессию Restart Manager.
- Добавить интересующий нас файл в сессию.
- Запросить список процессов, влияющих на ресурс.
- Напечатать немного информации по каждому процессу.
- Закрыть сессию.
RmStartSession. Следующим шагом мы добавляем в сессию единственный файл вызовом RmRegisterResources.Теперь начинается веселье. Получение списка процессов обычно происходит в два этапа. Сначала вы запрашиваете число доступных процессов (передавая 0 в
nProcInfo), затем выделяете память и вызываете функцию второй раз для получения данных. Но поскольку это просто пример, я вшил в программу фиксированное число процессов. Если файл открыт более 10 процессами, то я просто сдамся (вы можете проверить это, запустив программу и указав файл вроде kernel32.dll).Прим.пер.: и если какой-то сильно умный читатель вздумает скопировать этот код и использовать "как есть"...
Вторая хитрая часть заключается в поиске процесса по
TRMProcessInfo. Поскольку ID процессов могут использоваться заново (recycle), то структура TRMProcessInfo идентифицирует процесс комбинацией ID и времени его запуска. Такая комбинация является уникальной в рамках одной машины, потому что два процесса не могут иметь одинаковые ID в одно и то же время. Поэтому мы открываем процесс по его ID, а затем проверяем, что это именно тот процесс, что нам нужен (а если нет - то ID ссылается на процесс, который уже завершил работу с того момента, когда мы запрашивали список). Если же все данные совпали, то мы выводим путь к .exe файлу процесса.Вот и всё, что нужно, чтобы перечислить все процессы, открывшие какой-то конкретный файл.
Конечно же, более выразительным интерфейсом для управления используемыми файлами является
IFileIsInUse, который я упоминал не так давно. Этот интерфейс скажет вам не только какие приложения открыли файл (и в более дружелюбном формате, чем просто путь к .exe), но вы также сможете переключиться на это приложение и даже попросить его закрыть файл (если оно поддерживает эту возможность). Сама Windows 7 сначала пытается использовать IFileIsInUse, и лишь если он не сумел освободить файл - обращается к Restart Manager.Читать далее: использование
IFileIsInUse.
Где-то я видел подобное описание. Даже слово в слово. Только на английском и с++. Вот же оно: http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx
ОтветитьУдалитьЭэээ... слова "это перевод... автор - Реймонд Чен" ни о чём не говорят? :)
ОтветитьУдалитьНу хоть что-то более менее нормальное появилось да еще и документированное.
ОтветитьУдалитьА раньше приходилось ручками все делать :)
http://rouse.drkb.ru/winapi.php#enumopenfiles
FillChar(szSessionKey, SizeOf(szSessionKey), 0);
ОтветитьУдалить