Search code examples
windowsdelphidelphi-10.2-tokyo

Error: "Cannot open file "20210609.log". The process cannot access the file because it is being used by another process"


I've got some third party code that writes to a log file one line at a time using the code below:

procedure TLog.WriteToLog(Entry: ansistring);
var
    strFile: string;
    fStream: TFileStream;
    strDT: ansistring;
begin
    if ((strLogDirectory<>'') and (strFileRoot<>'')) then
    begin
        if not(DirectoryExists(strLogDirectory)) then
            ForceDirectories(strLogDirectory);
        strFile:=strLogDirectory + '\' + strFileRoot + '-' + strFilename;
        if FileExists(strFile) then
            fStream:=TFileStream.Create(strFile, fmOpenReadWrite)
        else
            fStream:=TFileStream.Create(strFile, fmCreate);
        fStream.Seek(0, soEnd);
        if blnUseTimeStamp then
            strDT:=formatdatetime(strDateFmt + ' hh:mm:ss', Now) + ' ' + Entry + chr(13) + chr(10)
        else
            strDT:=Entry + chr(13) + chr(10);
        fStream.WriteBuffer(strDT[1], length(strDT));
        FreeandNil(fStream);
    end;
end;

This has previously been working fine at a client site but in the last few weeks it is now getting the error in the title.

There is no other process that should have the file open. I suspect it is Anti-Virus but the client claims they have disabled the AntiV and they still get the error.

The error ONLY seems to occur when the code is in a loop and writing lines fast.

WHAT I WANT TO KNOW: Assuming it is not the Anti-Virus (or similar) causing the problem, could it be due to the operating system not clearing a flag (or something similar) before the next time it tries to write to the file?


Solution

  • WHAT I WANT TO KNOW: Assuming it is not the Anti-Virus (or similar) causing the problem, could it be due to the operating system not clearing a flag (or something similar) before the next time it tries to write to the file?

    No, this is not a speed issue, or a caching issue. It is a sharing violation, which means there MUST be another open handle to the same file, where that handle has sharing rights assigned (or lack of) which are incompatible with the rights being requested by this code.

    For example, if that other handle is not sharing read+write access, then this code will fail to open the file when creating the TFileStream with fmOpenReadWrite. If any handle is open to the file, this code will fail when creating the TFileStream with fmCreate, as that requests Exclusive access to the file by default.

    I would suggest something more like this instead:

    procedure TLog.WriteToLog(Entry: AnsiString);
    var
      strFile: string;
      fStream: TFileStream;
      strDT: AnsiString;
      fMode: Word;
    begin
      if (strLogDirectory <> '') and (strFileRoot <> '') then
      begin
        ForceDirectories(strLogDirectory);
        strFile := IncludeTrailingPathDelimiter(strLogDirectory) + strFileRoot + '-' + strFilename;
        fMode := fmOpenReadWrite or fmShareDenyWrite;
        if not FileExists(strFile) then fMode := fMode or fmCreate;
        fStream := TFileStream.Create(strFile, fMode);
        try
          fStream.Seek(0, soEnd);
          if blnUseTimeStamp then
            strDT := FormatDateTime(strDateFmt + ' hh:mm:ss', Now) + ' ' + Entry + sLineBreak
          else
            strDT := Entry + sLineBreak;
          fStream.WriteBuffer(strDT[1], Length(strDT));
        finally
          fStream.Free;
        end;
      end;
    end;
    

    However, do note that using FileExists() introduces a TOCTOU race condition. The file might be deleted/created by someone else after the existence is checked and before the file is opened/created. Best to let the OS handle this for you.

    At least on Windows, you can use CreateFile() directly with the OPEN_ALWAYS flag (TFileStream only ever uses CREATE_ALWAYS, CREATE_NEW, or OPEN_EXISTING), and then assign the resulting THandle to a THandleStream, eg:

    procedure TLog.WriteToLog(Entry: AnsiString);
    var
      strFile: string;
      hFile: THandle;
      fStream: THandleStream;
      strDT: AnsiString;
    begin
      if (strLogDirectory <> '') and (strFileRoot <> '') then
      begin
        ForceDirectories(strLogDirectory);
        strFile := IncludeTrailingPathDelimiter(strLogDirectory) + strFileRoot + '-' + strFilename;
        hFile := CreateFile(PChar(strFile), GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_ALWAYS, 0, 0);
        if hFile = INVALID_HANDLE_VALUE then RaiseLastOSError;
        try
          fStream := THandleStream.Create(hFile);
          try
            fStream.Seek(0, soEnd);
            if blnUseTimeStamp then
              strDT := FormatDateTime(strDateFmt + ' hh:mm:ss', Now) + ' ' + Entry + sLineBreak
            else
              strDT := Entry + sLineBreak;
            fStream.WriteBuffer(strDT[1], Length(strDT));
          finally
            fStream.Free;
          end;
        finally
          CloseHandle(hFile);
        end;
      end;
    end;
    

    In any case, you can use a tool like SysInternals Process Explorer to verify if there is another handle open to the file, and which process it belongs to. If the offending handle in question is being closed before you can see it in PE, then use a tool like SysInternals Process Monitor to log access to the file in real-time and check for overlapping attempts to open the file.