Корректное чтение файлов, в которые периодически заняты сторонним приложением.

Андрей Игоревич
Дата: 26.11.2019 18:46:58
Есть программы, которые периодически скидывают данные в текстовые файлы, есть одна программа, которая периодически эти данные считывает.
Записываются файлы классическим WriteLn, считываются TSringList.LoadFromFile();
Но! Иногда при считывании вылетала ошибка о том, что файл открыт на запись (что логично), на данный момент обыграл функцией проверяющей доступность файла, вроде не вылетает ошибка, но может просто везет.
+ функция проверки доступности FileIsUse
function FileIsUse(fName: string): boolean;   //проверка файла на открытия не запись
  var
    HFileRes: HFILE;
  begin
    Result := false;
    if not FileExists(fName) then exit;
    HFileRes := CreateFile(pchar(fName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    Result := (HFileRes = INVALID_HANDLE_VALUE);
    if not Result then CloseHandle(HFileRes);
  end;

В коде делают так
     while FileIsUse (FilePath) do Sleep(10);
     ListFile.LoadFromFile(FilePath);

Но как-то сомнения, что так правильно.

Ну и обратный вопрос LoadFromFile() не блокирует файл для записи? (что гораздо страшнее, так записывающую программу ломать очень не хочется)

_Vasilisk_

Андрей Игоревич
 while FileIsUse (FilePath) do Sleep(10);
это полные бред

Делаю так из соображений, что запись происходит в файл крайне редко (раз в десятки минут, гарантированно не чаще), если этот момент пропустить - чтение однозначно будет произведено, чтение однозначно занимает милисекунды.

На всех форумах предлагают варианты с обработкой исключений, но неужели иначе нельзя. И насколько правильно обрабатывать исключения в потоках? Никаких подводных камней?

В принципе в случае, если файл недоступен мне просто надо подождать.
Квейд
Дата: 26.11.2019 18:50:50
так не поможет?

FileStream := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite);
try
  ListFile.LoadFromStream(FileStream);
finally
  FileStream.Free
end
Kazantsev Alexey
Дата: 26.11.2019 18:59:55
Квейд
так не поможет?

Так, либо не откроет, при активном писателе, либо заблокирует возможность писать на время чтения. fmShareDenyNone нужно, это разрешит чтение во время записи и не будет блокировать писателя, если он не открывает файл с эксклюзивными правами.
_Vasilisk_
Дата: 26.11.2019 19:58:37
Андрей Игоревич
LoadFromFile() не блокирует файл для записи?
Смотрим в код
procedure TStrings.LoadFromFile(const FileName: string);
var
  Stream: TStream;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
end;
ответ - да, блокирует
Андрей Игоревич
Делаю так из соображений, что запись происходит в файл крайне редко (раз в десятки минут, гарантированно не чаще), если этот момент пропустить - чтение однозначно будет произведено, чтение однозначно занимает милисекунды.
Сценарий:

1. Вы вызвали FileIsUse. Файл был свободен.
2. Другая программа заняла файла
3. Вы вызвали ListFile.LoadFromFile(FilePath); и получили исключение.

Андрей Игоревич
И насколько правильно обрабатывать исключения в потоках? Никаких подводных камней?
Если правильно обрабатывать, то подводных камней нет.

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

Как я бы решал задачу:

Правильно и универсально - доступ к файлу синхронизирован общим мьютексом. Тогда каждый будет ждать, когда второй файл закроет.

Правильно, но не универсально. Исходим из утверждения
Андрей Игоревич
что запись происходит в файл крайне редко

Вызвал бы ReadDirectoryChangesW() с маской, скажем FILE_NOTIFY_CHANGE_LAST_WRITE (или FILE_NOTIFY_CHANGE_LAST_ACCESS - нужно смотреть, что будет адекватнее), дождался бы окончания записи и спокойно прочитал бы файл

Не правильно, не универсально, но быстро
Дважды прочитал бы файл без монопольного доступа. Если два раза была считана одна и та же информация - файл прочитан корректно

+
type
  {$IFDEF UNICODE}
  TBuffer = TBytes;
  {$ELSE}
  TBuffer = string;
  {$ENDIF}

function OpenFile(const AName: string): THandle;
begin
  Result := CreateFile(
    PChar(AName),
    GENERIC_READ,
    FILE_SHARE_READ or FILE_SHARE_WRITE,
    nil,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0
  );
end;

function ReadFile(const AName: string): TBuffer;
var
  LFile: THandle;
  LSize: Cardinal;
  LReadSize: Cardinal;
begin
  LFile := OpenFile(AName);
  while LFile = INVALID_HANDLE_VALUE do begin
    Sleep(100);
    LFile := OpenFile(AName);
  end;
  try
    LSize := GetFileSize(AFile, nil);
    if LSize = INVALID_FILE_SIZE then
      RaiseLastOSError;
    SetLength(Result, LSize);
    Win32Check(ReadFile(AFile, Pointer(Result), LSize, @LReadSize, nil));
    if LSize <> LReadSize then
      SetLength(Result, LReadSize);
  finally
    CloseHandle(LFile);
  end;
end;

function SameBuff(const ABuf1, ABuf2: TBuffer): Boolean;
begin
  Result := 
    (Length(ABuf1) = Length(ABuf2)) and
    CompareMem(Pointer(ABuf1), Pointer(ABuf2), Length(ABuf1));
end;

procedure LoadToList(const AName: string; AList: TStrings);
var
  LBuf1: TBuffer;
  LBuf2: TBuffer;
  LCurBuf: ^TBuffer;
begin
  LBuf1 := ReadFile(AName);
  LCurBuf := @LBuf2;
  repeat
    LCurBuf^ := ReadFile(AName);
    if LCurBuf = @LBuf2 then
      LCurBuf := @LBuf1
    else
      LCurBuf := @LBuf2;
  until SameBuff(LBuf1, LBuf2);
  {$IFDEF UNICODE}
  AList.Text := TEncoding.Default.GetString(LBuf1);
  {$ELSE}
  AList.Text := LBuf1;
  {$ENDIF}
end;
ёёёёё
Дата: 26.11.2019 20:00:00
Андрей Игоревич
На всех форумах предлагают варианты с обработкой исключений, но неужели иначе нельзя. И насколько правильно обрабатывать исключения в потоках? Никаких подводных камней?

Никаких камней, все как везде - главное не писать в общие данные одновременно.
...
Ты не борись с ситуацией, а не создавай её. Пусть "пишущие" приложение пишет в один файл, а "читающее" - читает из другого.
Первое пусть пишет в файл с расширением .pre, потом закрывает файл и переименовывает - меняет расширение на .ready, а второе читает из готового *.ready файла.
Андрей Игоревич
Дата: 26.11.2019 20:18:31
ёёёёё
Андрей Игоревич
На всех форумах предлагают варианты с обработкой исключений, но неужели иначе нельзя. И насколько правильно обрабатывать исключения в потоках? Никаких подводных камней?

Никаких камней, все как везде - главное не писать в общие данные одновременно.
...
Ты не борись с ситуацией, а не создавай её. Пусть "пишущие" приложение пишет в один файл, а "читающее" - читает из другого.
Первое пусть пишет в файл с расширением .pre, потом закрывает файл и переименовывает - меняет расширение на .ready, а второе читает из готового *.ready файла.

Пишущая программа мне почти недоступна, вносить туда правки сложное дело. Хотя конкретно такую правку может и договорюсь внести, вроде ничего радикального.
Андрей Игоревич
Дата: 27.11.2019 09:27:48
_Vasilisk_
Андрей Игоревич
LoadFromFile() не блокирует файл для записи?
Смотрим в код
procedure TStrings.LoadFromFile(const FileName: string);
var
  Stream: TStream;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
end;
ответ - да, блокирует

Тут ок, сделаю через TFileStream.Create(name, fmOpenRead or fmShareDenyNone), просто удивлен, зачем его тут для записи открывают, если для записи есть SaveToFile;

_Vasilisk_
Сценарий:

1. Вы вызвали FileIsUse. Файл был свободен.
2. Другая программа заняла файла
3. Вы вызвали ListFile.LoadFromFile(FilePath); и получили исключение.


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

_Vasilisk_

Правильно и универсально - доступ к файлу синхронизирован общим мьютексом. Тогда каждый будет ждать, когда второй файл закроет.

Для этого надо править записывающую программу - а это уже отдельная проблема.
_Vasilisk_

Правильно, но не универсально. Исходим из утверждения
Андрей Игоревич
что запись происходит в файл крайне редко

Вызвал бы ReadDirectoryChangesW() с маской, скажем FILE_NOTIFY_CHANGE_LAST_WRITE (или FILE_NOTIFY_CHANGE_LAST_ACCESS - нужно смотреть, что будет адекватнее), дождался бы окончания записи и спокойно прочитал бы файл

Не правильно, не универсально, но быстро
Дважды прочитал бы файл без монопольного доступа. Если два раза была считана одна и та же информация - файл прочитан корректно

+
type
  {$IFDEF UNICODE}
  TBuffer = TBytes;
  {$ELSE}
  TBuffer = string;
  {$ENDIF}

function OpenFile(const AName: string): THandle;
begin
  Result := CreateFile(
    PChar(AName),
    GENERIC_READ,
    FILE_SHARE_READ or FILE_SHARE_WRITE,
    nil,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0
  );
end;

function ReadFile(const AName: string): TBuffer;
var
  LFile: THandle;
  LSize: Cardinal;
  LReadSize: Cardinal;
begin
  LFile := OpenFile(AName);
  while LFile = INVALID_HANDLE_VALUE do begin
    Sleep(100);
    LFile := OpenFile(AName);
  end;
  try
    LSize := GetFileSize(AFile, nil);
    if LSize = INVALID_FILE_SIZE then
      RaiseLastOSError;
    SetLength(Result, LSize);
    Win32Check(ReadFile(AFile, Pointer(Result), LSize, @LReadSize, nil));
    if LSize <> LReadSize then
      SetLength(Result, LReadSize);
  finally
    CloseHandle(LFile);
  end;
end;

function SameBuff(const ABuf1, ABuf2: TBuffer): Boolean;
begin
  Result := 
    (Length(ABuf1) = Length(ABuf2)) and
    CompareMem(Pointer(ABuf1), Pointer(ABuf2), Length(ABuf1));
end;

procedure LoadToList(const AName: string; AList: TStrings);
var
  LBuf1: TBuffer;
  LBuf2: TBuffer;
  LCurBuf: ^TBuffer;
begin
  LBuf1 := ReadFile(AName);
  LCurBuf := @LBuf2;
  repeat
    LCurBuf^ := ReadFile(AName);
    if LCurBuf = @LBuf2 then
      LCurBuf := @LBuf1
    else
      LCurBuf := @LBuf2;
  until SameBuff(LBuf1, LBuf2);
  {$IFDEF UNICODE}
  AList.Text := TEncoding.Default.GetString(LBuf1);
  {$ELSE}
  AList.Text := LBuf1;
  {$ENDIF}
end;

В каталоге до полутора тысяч файлов разных видов которые записываются в разное время (хотя между записью/обновлением одного конкретного файла времени проходит много), такой способ интересен, но очень уж сложен :).
В общем сделаю через TFileStream.Create с флагом без ограничения доступа и обработаю исключение.
Спасибо большое за помощь.
alekcvp
Дата: 27.11.2019 10:43:51
Андрей Игоревич

Ну по такому сценарию файл может быт заблокирован прям в процессе чтения, тогда уж, действительно, ничего кроме исключения не спасет.
Не может. При попытке его заблокировать, блокировщик получит ошибку.
Василий 2
Дата: 27.11.2019 10:58:25
Андрей Игоревич

Тут ок, сделаю через TFileStream.Create(name, fmOpenRead or fmShareDenyNone), просто удивлен, зачем его тут для записи открывают, если для записи есть SaveToFile;

Его не открывают для записи, а блокируют ОТ записи. Чтобы в процессе чтения кто-то другой не перезаписал содержимое. А вот если открывать с fmShareDenyNone, то такая возможность существует
ёёёёё
Дата: 27.11.2019 12:03:34
Андрей Игоревич
fmShareDenyNone)

Ну и получишь вместо блокировки неконсистентные данные.