04 марта 2020

Избавляемся от "Процесс не может получить доступ к файлу, так как этот файл занят другим процессом"

    Процесс не может получить доступ к файлу, так как этот файл занят другим процессом... Но это не точно... Как показала практика, для программы на Delphi это действительно не точно.
    При открытии файла в режиме "только для чтения" в параметре определяющем режим доступа к файлу обычно передают комбинацию "fmOpenRead or fmShareDenyNone". Но оказалось, что это избавляет только от части ошибок "Процесс не может получить доступ к файлу, так как этот файл занят другим процессом" или "The process cannot access the file because it is being used by another process".
    Для начала немного теории. В Delphi режим доступа к файлу, определяется комбинацией константы режима открытия файла (fmOpen*) и константы разделения доступа к файлу (fmShare*). Эти константы описанны в модуле System.SysUtils. Приведу их значение для ОС Windows:

Наименование Значение Описание
Режимы открытия файла
fmOpenRead $0000 Открытие файла только для чтения
fmOpenWrite $0001 Открытие файла только для записи
fmOpenReadWrite $0002 Открытие файла для чтения и записи
Режимы разделения доступа
fmShareExclusive $0010 Другие приложения не имеют доступа к файлу
fmShareDenyWrite $0020 Другие приложения могут только читать файл
fmShareDenyRead $0030 Другие приложения могут только писать в файл
fmShareDenyNone $0040 Другие приложения могут читать файл и писать в него

    Теперь перейдем к практике. Попробуем в режиме fmOpenRead открыть все файлы в каталоге для хранения временных файлов (там большая вероятность встретить файл, который занят какими-нибудь процессом).

Вариант 1. Откроем файлы без указания режима разделения доступа
try
  with TFileStream.Create(sFileName, fmOpenRead) do
    try
      // читаем секретные материалы
    finally
      Free
    end;
except
  on E: Exception do
    Writeln(E.Message);
end;
У меня в каталоге для хранения временных файлов нашлось 23 файла, которые заняты какими-то процессами.

Вариант 2. Откроем файлы в режиме разделения доступа "fmShareDenyNone"
try
  with TFileStream.Create(sFileName, fmOpenRead or fmShareDenyNone) do
    try
      // истина где-то рядом
    finally
      Free
    end;
except
  on E: Exception do
    Writeln(E.Message);
end;
Теперь получаем 16 файлов занятых другими процессами и радуемся улучшению результата. Радуемся, но не долго. Некоторые из этих "занятых" файлов открываются на просмотр в "Lister" из Total Commander! Как так?!?

Метод TFileStream.Create приводит к функции FileOpen из System.SysUtils:
function FileOpen(const FileName: string; Mode: LongWord): THandle;
const
  AccessMode: array[0..2] of LongWord = (
    GENERIC_READ,
    GENERIC_WRITE,
    GENERIC_READ or GENERIC_WRITE);
  ShareMode: array[0..4] of LongWord = (
    0,
    0,
    FILE_SHARE_READ,
    FILE_SHARE_WRITE,
    FILE_SHARE_READ or FILE_SHARE_WRITE);
begin
  Result := INVALID_HANDLE_VALUE;
  if ((Mode and 3) <= fmOpenReadWrite) and
    ((Mode and $F0) <= fmShareDenyNone) then
    Result := CreateFile(PChar(FileName), AccessMode[Mode and 3],
      ShareMode[(Mode and $F0) shr 4], nil, OPEN_EXISTING,
      FILE_ATTRIBUTE_NORMAL, 0);
end;
Получается, что в режиме разделения доступа fmShareDenyNone функция CreateFile вызывается с параметром dwShareMode = FILE_SHARE_READ or FILE_SHARE_WRITE. Согласно документации от Microsoft, в WinAPI, кроме FILE_SHARE_READ и FILE_SHARE_WRITE, есть еще режим разделения доступа FILE_SHARE_DELETE. В Delphi константа FILE_SHARE_DELETE описана в модуле Winapi.Windows, но не используется. Откроем файл для нашего потока сами с учетом FILE_SHARE_DELETE:
var
  h: THandle;
...
h := CreateFile(PChar(sFileName),
                GENERIC_READ,
                FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
                nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if h = INVALID_HANDLE_VALUE
  then Writeln(SysErrorMessage(GetLastError))
  else with TFileStream.Create(h) do
         try
           // X-файлы наши!
         finally
           Free
         end;
Оказывается, что у меня в каталоге для хранения временных файлов реально только 4 файла занятых другими процессами!
 
В результате я добавил себе в проект функцию
function FileOpenAsReadOnly(const sFileName: String): THandle;
begin
  Result := CreateFile(PChar(sFileName),
                       GENERIC_READ,
                       FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
                       nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if Result = INVALID_HANDLE_VALUE then
    raise EFOpenError.CreateResFmt(@SFOpenErrorEx, 
                                   [ExpandFileName(sFileName), 
                                   SysErrorMessage(GetLastError)])
end;
и использовал ее для открытия файлов в режиме "только для чтения":
TFileStream.Create(FileOpenAsReadOnly(sFileName))
    Конечно, полностью победить эту ошибку мне не удалось, но 4 файла против 23 или 16 - это тоже хорошее достижение. Интересно, то, что разработчики из Borland потеряли FILE_SHARE_DELETE, а разработчики из Embarcadero не нашли - это ошибка в Delphi? Или так надо, и я чего-то не понимаю?
 

Исходный текст тестовой программы: FileOpenTest.dpr

1 комментарий:

  1. Проблема известна давно и в некоторых случаях решается указанным вами способом, но, к сожалению, не универсальна. Microsoft большая и там случаются нарушители конвенций. У меня есть надежно повторяемый пример с PowerShell, когда CreateFile с любыми параметрами показывает, что файл свободен, но реально он занят. Приходится тупо использовать эмпирический Sleep.

    al

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