Исторически, нет никакого специального способа найти процесс, который держит файл. Файловый объект имеет обычный счётчик ссылок объекта ядра и когда счётчик опускается до нуля - файл закрывается. Но в системе никто не отслеживает процессы, открывшие данный описатель, и сколько именно раз они его открыли (и это упрощённое изложение даже игнорирует тот факт, что счётчик может быть увеличен вовсе не процессом, а, скажем, драйвером режима ядра; или, быть может, изначально счётчик был увеличен процессом, который теперь уже закрыт, но файл ещё держится драйвером ядра).
Это состояние вещей согласуется с концепцией не хранить информацию, которая вам не нужна. Файловую систему не заботит, кто там держит её файлы. Её задача - закрыть файл, когда уйдёт последняя ссылка.
Аналогичную ситуацию вы видите в 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);
ОтветитьУдалить