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

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

我有一些第三方代码可以使用以下代码一次一行地写入日志文件:

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;

这之前在客户站点上运行良好,但在过去几周内,它现在在标题中出现错误。

没有其他进程应该打开该文件。我怀疑是 Anti-Virus,但客户声称他们已禁用 AntiV,但他们仍然收到错误。

错误似乎只发生在代码处于循环中并快速写入行时。

我想知道的: 假设不是 Anti-Virus(或类似的)导致问题,是否可能是由于操作系统在下次尝试写入文件之前未清除标志(或类似的)?

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?

不,这不是速度问题,也不是缓存问题。这是共享违规,这意味着同一文件必须有另一个打开的句柄,其中该句柄分配了(或没有)共享权限,这与此代码请求的权限不兼容。

例如,如果其他句柄不共享读+写访问权限,则此代码在创建 TFileStreamfmOpenReadWrite 时将无法打开文件。如果 any 句柄对文件打开,此代码在使用 fmCreate 创建 TFileStream 时将失败,因为它请求 Exclusive 默认访问文件。

我会建议更像这样的东西:

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;

但是,请注意使用 FileExists() 会引入 TOCTOU race condition。在检查文件是否存在之后和 opened/created 之前,该文件可能被其他人 deleted/created。最好让 OS 为您处理。

至少在 Windows,你可以直接使用 CreateFile()OPEN_ALWAYS 标志(TFileStream 只使用 CREATE_ALWAYSCREATE_NEW,或OPEN_EXISTING),然后将结果THandle赋值给一个THandleStream,例如:

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;

在任何情况下,您都可以使用 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 之类的工具来实时记录对文件的访问并检查打开文件的重叠尝试。